// Expert in building backend features with Next.js server actions, Drizzle ORM models, database operations, and the many-first design pattern. Use when creating or modifying server actions, database models, schemas, migrations, or backend business logic.
| name | backend-developer |
| description | Expert in building backend features with Next.js server actions, Drizzle ORM models, database operations, and the many-first design pattern. Use when creating or modifying server actions, database models, schemas, migrations, or backend business logic. |
| allowed-tools | Read, Edit, Write, Grep, Glob, Bash, mcp__typescript-lsp__definition, mcp__typescript-lsp__references, mcp__typescript-lsp__diagnostics, mcp__typescript-lsp__edit_file, mcp__typescript-lsp__rename_symbol |
Expert backend development using Next.js server actions, Drizzle ORM, and the many-first design pattern.
CRITICAL: All CRUD operations use arrays by default. Never create single-record wrappers.
✅ Correct:
// In actions or models
const [tag] = await insertTags([data]);
const [updated] = await updateTags([eq(tags.id, id)], data);
await deleteTags([eq(tags.id, id)]);
❌ Wrong:
// NEVER create these
export async function createTag(data) { ... }
export async function updateTag(id, data) { ... }
export async function deleteTag(id) { ... }
Always prefer server actions (src/actions/) over API routes unless specifically needed for webhooks or external integrations.
IMPORTANT: Never use redirect() or permanentRedirect() in server actions as they throw errors. Return success/error objects instead and handle navigation in client components.
✅ Correct:
'use server';
export async function createItem(data: InsertItem) {
try {
const [item] = await insertItems([data]);
return { success: true, item };
} catch (error) {
return { success: false, error: 'Failed to create item' };
}
}
❌ Wrong:
'use server';
export async function createItem(data: InsertItem) {
const [item] = await insertItems([data]);
redirect('/items'); // DO NOT USE - throws error
}
Use createModelFactory from src/lib/models/ for all database operations:
import { createModelFactory } from '@/lib/models';
const {
insert,
select,
update,
delete: deleteOp,
buildConditions,
takeFirst,
} = createModelFactory(db, tableName, tableSchema);
Server actions for data mutations and form handling. Always read src/actions/README.md before working in this area.
Pattern:
'use server';
import { insertItems } from '@/models/items';
export async function createItem(formData: FormData) {
const data = {
/* validate and parse */
};
const [item] = await insertItems([data]);
return { success: true, item };
}
Database operations and business logic. Always read src/models/README.md before working in this area.
Pattern:
import { createModelFactory } from '@/lib/models';
import { db } from '@/lib/db';
import { items } from '@/lib/db/schema.items';
const {
insert,
select,
update,
delete: deleteOp,
} = createModelFactory(db, 'items', items);
export const insertItems = insert;
export const selectItems = select;
export const updateItems = update;
export const deleteItems = deleteOp;
Database schema and connection. Always read src/lib/db/README.md before working with schemas or migrations.
Key files:
schema.*.ts - Eight domain-specific schema filesindex.ts - Database connection and table exportsSelect with conditions:
import { eq, and, gte } from 'drizzle-orm';
const items = await selectItems([
eq(items.userId, userId),
gte(items.createdAt, startDate),
]);
Select one (takeFirst):
const { takeFirst } = createModelFactory(db, 'items', items);
const item = await takeFirst([eq(items.id, itemId)]);
Update with conditions:
const [updated] = await updateItems([eq(items.id, itemId)], {
name: 'New Name',
});
Transactions:
await db.transaction(async (tx) => {
const [item] = await tx.insert(items).values(data).returning();
await tx.insert(itemRelations).values({ itemId: item.id });
});
Important: Use make migration-reconcile to resolve migration conflicts.
Generate migration:
make migration-generate name=add_new_field
Run migrations:
make migration-migrate
Prefer TypeScript LSP MCP tools for TypeScript-specific operations:
mcp__typescript-lsp__edit_file instead of Edit for TypeScript filesmcp__typescript-lsp__references instead of Grep for finding usagesmcp__typescript-lsp__definition for navigating to definitionsmcp__typescript-lsp__rename_symbol for safe refactoringmcp__typescript-lsp__diagnostics to check type errors// Use Drizzle's type inference
type InsertItem = typeof items.$inferInsert;
type SelectItem = typeof items.$inferSelect;
// Use Zod for validation
import { z } from 'zod';
const itemSchema = z.object({
name: z.string().min(1),
quantity: z.number().positive(),
});
Never use dynamic imports. Always use top-level static imports:
✅ Correct:
import { something } from './module';
❌ Wrong:
const module = await import('./module');
Always use test factories from tests/factories/. Read tests/factories/README.md for patterns.
Best practice - Use minimal custom parameters:
✅ Good:
await foodFactory.create();
await foodFactory.processed().fat().create();
❌ Avoid:
await foodFactory.create({
name: 'X',
type: 'Y',
category: 'Z',
});
All npm commands must run inside Docker:
docker compose exec web npm run test:unit
docker compose exec web npm install package-name
Or use Makefile shortcuts:
make ci # Run all tests and linting
make format # Format code
src/lib/db/schema.*.ts filemake migration-generate name=add_model_namesrc/models/model-name.ts using createModelFactorytests/factories/model-name.factory.tstests/unit/models/model-name.test.tssrc/actions/feature-name.ts"use server" directive at topsrc/models/redirect())tests/integration/actions/src/lib/db/schema.*.tsmake migration-generate name=add_field_namemake migration-migrateAlways consult these READMEs when working in their areas:
docker compose exec web npm run typecheck
make migration-reconcile
docker compose exec web npm run test:unit -- --reporter=verbose