| 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 |
Backend Developer Skill
Expert backend development using Next.js server actions, Drizzle ORM, and the many-first design pattern.
Core Principles
1. Many-First Design Pattern
CRITICAL: All CRUD operations use arrays by default. Never create single-record wrappers.
✅ Correct:
const [tag] = await insertTags([data]);
const [updated] = await updateTags([eq(tags.id, id)], data);
await deleteTags([eq(tags.id, id)]);
❌ Wrong:
export async function createTag(data) { ... }
export async function updateTag(id, data) { ... }
export async function deleteTag(id) { ... }
2. Server Actions Over API Routes
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');
}
3. Model Factory Pattern
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);
Directory Structure & Responsibilities
src/actions/
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 = {
};
const [item] = await insertItems([data]);
return { success: true, item };
}
src/models/
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;
src/lib/db/
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 files
index.ts - Database connection and table exports
- Migration commands in Makefile
Database Operations
Drizzle Query Patterns
Select 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 });
});
Migrations
Important: Use make migration-reconcile to resolve migration conflicts.
Generate migration:
make migration-generate name=add_new_field
Run migrations:
make migration-migrate
TypeScript Best Practices
Use TypeScript LSP Tools
Prefer TypeScript LSP MCP tools for TypeScript-specific operations:
mcp__typescript-lsp__edit_file instead of Edit for TypeScript files
mcp__typescript-lsp__references instead of Grep for finding usages
mcp__typescript-lsp__definition for navigating to definitions
mcp__typescript-lsp__rename_symbol for safe refactoring
mcp__typescript-lsp__diagnostics to check type errors
Type Safety
type InsertItem = typeof items.$inferInsert;
type SelectItem = typeof items.$inferSelect;
import { z } from 'zod';
const itemSchema = z.object({
name: z.string().min(1),
quantity: z.number().positive(),
});
Avoid Dynamic Imports
Never use dynamic imports. Always use top-level static imports:
✅ Correct:
import { something } from './module';
❌ Wrong:
const module = await import('./module');
Testing
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',
});
Docker Commands
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
make format
Common Tasks
Create a new model
- Define schema in appropriate
src/lib/db/schema.*.ts file
- Generate migration:
make migration-generate name=add_model_name
- Create model file in
src/models/model-name.ts using createModelFactory
- Create factory in
tests/factories/model-name.factory.ts
- Write tests in
tests/unit/models/model-name.test.ts
Create a server action
- Create file in
src/actions/feature-name.ts
- Add
"use server" directive at top
- Import model operations from
src/models/
- Return success/error objects (never use
redirect())
- Write integration tests in
tests/integration/actions/
Add a database field
- Update schema in
src/lib/db/schema.*.ts
- Generate migration:
make migration-generate name=add_field_name
- Review and run migration:
make migration-migrate
- Update TypeScript types (auto-inferred from schema)
- Update factories if needed
Reference Documentation
Always consult these READMEs when working in their areas:
Troubleshooting
Type errors after schema change
docker compose exec web npm run typecheck
Migration conflicts
make migration-reconcile
Test failures
docker compose exec web npm run test:unit -- --reporter=verbose