Best Practices
Do's and Don'ts, proven patterns, anti-patterns to avoid, and performance optimization tips for production collections.
Best Practices
Guidelines for writing maintainable, performant, and type-safe collections code.
✅ Do's
1. Use Descriptive Collection Names
// ✅ Clear, plural, domain-specific
const BlogPosts = IgniterCollectionModel.create('posts')
const ProductPages = IgniterCollectionModel.create('products')
const UserProfiles = IgniterCollectionModel.create('profiles')
// ❌ Vague or abbreviated
const P = IgniterCollectionModel.create('p')
const Data = IgniterCollectionModel.create('data')2. Centralize Schema Definitions
// ✅ Define schemas separately for reuse
// schemas/post.schema.ts
export const postSchema = z.object({
title: z.string().min(1),
published: z.boolean().default(false),
tags: z.array(z.string()).default([]),
});
// collections/posts.ts
import { postSchema } from '../schemas/post.schema';
const Posts = IgniterCollectionModel.create('posts')
.withSchema(postSchema)
.build();
// Also use for type exports
export type Post = z.infer<typeof postSchema>;3. Always Paginate
// ✅ Paginate every list query
const posts = await docs.posts.findMany({
take: 20,
skip: (page - 1) * 20,
orderBy: { createdAt: 'desc' as const },
});
// ❌ Loading unbounded results
const allPosts = await docs.posts.findMany();4. Use Select for Network Efficiency
// ✅ Return only what you need
const titles = await docs.posts.findMany({
select: { id: true, title: true, slug: true },
take: 50,
});
// ❌ Return everything including content
const posts = await docs.posts.findMany({ take: 50 });
// Each document includes the full content body5. Handle Errors Gracefully
// ✅ Structured error handling
import { IgniterCollectionError } from '@igniter-js/collections';
try {
await docs.posts.create({ data: { title: 'Hello' } });
} catch (error) {
if (error instanceof IgniterCollectionError) {
switch (error.code) {
case 'COLLECTION_VALIDATION_ERROR':
return { error: 'Invalid data', details: error.details };
case 'COLLECTION_HOOK_CANCELLED':
return { error: 'Operation rejected by business rules' };
default:
return { error: 'Internal error' };
}
}
throw error; // Re-throw unexpected errors
}6. Clean Up Event Subscriptions
// ✅ Store handles for cleanup
class ContentService {
private handles: Array<() => void> = [];
start() {
this.handles.push(
docs.on('posts:created', this.onPostCreated).off
);
this.handles.push(
docs.on('posts:deleted', this.onPostDeleted).off
);
}
stop() {
this.handles.forEach(off => off());
this.handles = [];
}
}7. Use Context for Dependency Injection
// ✅ Inject dependencies through context
const docs = IgniterCollections.create()
.withAdapter(adapter)
.withContext(async () => ({
db: await Database.connect(),
cache: new Cache(),
}))
.addCollection(Posts)
.build();
Posts.onCreated(async ({ value, context }) => {
const { cache } = context as { cache: Cache };
cache.delete('posts:list');
return value;
});❌ Don'ts
1. Don't Use Hooks for Heavy Async Work
// ❌ Hooks block the operation
.onCreated(async ({ value }) => {
await sendEmail({ to: 'everyone@company.com' }); // Slow!
return value;
})
// ✅ Delegate to background processing
.onCreated(async ({ value }) => {
backgroundQueue.add('post-created', { id: value.id });
return value;
})2. Don't Modify Documents Outside the Manager
// ❌ Direct file manipulation bypasses hooks, validation, and events
import { writeFile } from 'fs/promises';
await writeFile('.content/posts/my-post.mdx', rawMarkdown);
// ✅ Always use the manager
await docs.posts.update({
where: { id: 'my-post' },
data: { title: 'Updated Title' },
});3. Don't Create Too Many Sub-Collections
// ❌ Deep nesting makes queries complex
Posts.collections.create('comments')
.collections.create('replies')
.collections.create('reactions')
.collections.create('emoji') // Too deep!
// ✅ Keep it to 2-3 levels max
Posts.collections.create('comments')
.collections.create('replies')4. Don't Use Zod transform in Schemas
// ❌ Transforms break type inference
const schema = z.object({
date: z.string().transform(s => new Date(s)),
});
// ✅ Transform in hooks instead
.onCreated(({ value }) => {
value.data.parsedDate = new Date(value.data.dateString);
return value;
})5. Don't Query Without Index Considerations
// ❌ Full scan on large collections
await docs.posts.findMany({
where: { author: 'Felipe' },
});
// ✅ Use pagination + full-text search where possible
await docs.posts.findMany({
where: {
author: 'Felipe',
search: { term: 'TypeScript' }, // Leverages MiniSearch index
},
take: 20,
});Performance Tips
Batch Operations
// ✅ Batch creates with Promise.all
const posts = await Promise.all(
importData.map(item =>
docs.posts.create({
data: {
title: item.title,
author: item.author,
published: false,
}
})
)
);Avoid N+1 with Sub-Collections
// ❌ N+1: fetching comments for each post individually
const posts = await docs.posts.findMany({ take: 10 });
for (const post of posts) {
const comments = await docs.posts.collections.get('comments').findMany({
where: { parentId: post.id },
});
}
// ✅ Use include
const posts = await docs.posts.findMany({
take: 10,
include: { comments: true },
});Caching Strategy
let cachedPosts: Post[] | null = null;
let cacheTimestamp = 0;
const CACHE_TTL = 60_000; // 1 minute
async function getPosts() {
if (cachedPosts && Date.now() - cacheTimestamp < CACHE_TTL) {
return cachedPosts;
}
cachedPosts = await docs.posts.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' as const },
take: 20,
});
cacheTimestamp = Date.now();
return cachedPosts;
}
// Invalidate on changes
docs.on('posts:created', () => { cachedPosts = null; });
docs.on('posts:updated', () => { cachedPosts = null; });Project Structure
Recommended file organization for a collections-powered project:
src/
├── collections/
│ ├── posts.ts # Post collection definition
│ ├── pages.ts # Page collection definition
│ └── authors.ts # Author collection definition
├── schemas/
│ ├── post.schema.ts # Zod schemas (reusable)
│ ├── page.schema.ts
│ └── author.schema.ts
├── views/
│ ├── dashboard.ts # Dashboard view definition
│ └── content-audit.ts # Audit view definition
├── hooks/
│ ├── slugify.ts # Hook: auto-generate slugs
│ └── audit-log.ts # Hook: audit trail
├── adapters/
│ └── storage.ts # Adapter factory (env-based)
└── manager.ts # Central manager bootstrap