| name | nextjs-fullstack-patterns |
| description | Full-stack Next.js backend-layer conventions: Drizzle schema and queries, route handlers, server actions, the service layer, Auth.js, and Redis/BullMQ. Apply when writing or reviewing the data/API layer of a full-stack Next.js app. |
Skill: Next.js Full-Stack Patterns
Purpose
Guide Claude Code when implementing full-stack Next.js features. This covers the backend layer — Drizzle ORM, Postgres, Redis, BullMQ, Auth.js, Route Handlers, and Server Actions. Pair with nextjs-patterns.md (loaded via the frontend profile) for component, styling, and naming conventions.
When using fullstack mode, ignore these sections of nextjs-patterns.md
- API Client — no
lib/api.ts or apiFetch wrapper; data access goes through the service layer directly
- Frontend-Centric Mode — no JWT/localStorage auth; use Auth.js instead
- The SSR-Centric Mode section still applies
Project Structure
src/
├── app/
│ ├── api/ # Route Handlers (REST endpoints)
│ │ ├── auth/[...nextauth]/route.ts
│ │ └── v1/
│ │ └── {resource}/route.ts
│ ├── (auth)/ # Auth pages (login, register, etc.)
│ ├── (dashboard)/ # Authenticated app pages
│ ├── layout.tsx
│ └── page.tsx
├── components/ # React components (managed by frontend profile)
├── lib/
│ ├── auth.ts # Auth.js configuration
│ ├── db/
│ │ ├── index.ts # Drizzle client (singleton)
│ │ ├── schema/ # Table definitions, one file per domain
│ │ │ ├── users.ts
│ │ │ ├── index.ts # Re-exports all tables
│ │ │ └── ...
│ │ └── migrations/ # Generated by drizzle-kit
│ ├── redis.ts # Redis client (singleton)
│ └── queue/
│ ├── client.ts # Shared BullMQ connection
│ ├── queues.ts # Queue definitions
│ └── workers.ts # Worker definitions
├── server/
│ ├── actions/ # Server Actions, one file per domain
│ │ ├── auth.ts
│ │ └── {resource}.ts
│ └── services/ # Business logic, one file per domain
│ ├── auth.ts
│ └── {resource}.ts
└── types/
└── index.ts # Shared types
Drizzle ORM
Schema
Queries
- Use the Drizzle query builder — not raw SQL
- Use
db.select() / db.insert() / db.update() / db.delete() for simple operations
- Use
db.query.tableName.findMany() / findFirst() with with for relational queries
- Always scope queries: filter by tenant/user, never return unscoped data
- Use
.$dynamic() for conditional query building
Migrations
- Generate with
drizzle-kit generate
- Apply with
drizzle-kit migrate
- Review generated SQL before committing — never blindly apply
- Never edit a migration that's been pushed to main
- Run migrations via Makefile:
make db-generate, make db-migrate
Client
- Single
db instance in lib/db/index.ts using drizzle(pool) with node-postgres
- Use connection pooling via
Pool — not single Client
- Export the typed
db for use in services
Auth.js (v5)
Setup
- Configuration in
lib/auth.ts — export { handlers, auth, signIn, signOut }
- Route handler at
app/api/auth/[...nextauth]/route.ts re-exports handlers
- Use the Drizzle adapter (
@auth/drizzle-adapter) for session/account storage
- Default to database sessions (not JWT) for fullstack apps
Protecting Routes
- Server Components: call
auth() to get the session, redirect if null
- Route Handlers: call
auth() at the top, return 401 if null
- Server Actions: call
auth() at the top, throw if null
- Middleware: use
auth as middleware in middleware.ts for blanket route protection
- Define public routes explicitly in middleware matcher config
Patterns
- Store minimal user data in the session — fetch full profile from DB when needed
- Extend the session type in
types/next-auth.d.ts if adding fields
- Use
auth() — never parse cookies or tokens manually
Route Handlers (API Routes)
Conventions
- Place under
app/api/v1/{resource}/route.ts
- Export named functions matching HTTP methods:
GET, POST, PUT, PATCH, DELETE
- Parse request body with
request.json() and validate with Zod
- Return
NextResponse.json() with appropriate status codes
- Route handlers call services — no business logic in the handler itself
Validation
Response Shape
- Collections use the canonical paginated envelope:
{ page, count, num_pages, results: T[] } with page/page_size query params (matches the cross-stack API contract)
- Single resources: return the object directly (no envelope)
- Errors:
{ error: string, details?: Record<string, string[]> }
Server Actions
Conventions
- Files in
server/actions/, one per domain, with "use server" at the top
- Name actions as verbs:
createItem, updateItem, deleteItem
- Always validate input with Zod before processing
- Always check auth via
auth() before any data mutation
- Actions call services — no business logic in the action itself
Return Pattern
Service Layer
Conventions
- Each domain has a service file in
server/services/
- Services are plain functions (not classes) that take explicit dependencies
- Services contain all business logic — route handlers and server actions are thin wrappers
- Services call Drizzle for data access, queue jobs, send emails, etc.
- Services never import from
next/headers or Next.js request APIs — they receive data as arguments
Testing
- Services are the primary unit-test target — test business logic without HTTP
- Mock the
db and redis clients, not the service functions themselves
- Test route handlers and server actions as integration tests
Redis
- Single client in
lib/redis.ts using ioredis
- Use for: caching, rate limiting, session storage (if needed beyond Auth.js), BullMQ connection
- Key naming convention:
{app}:{domain}:{id} (e.g., myapp:user:123:profile)
- Always set TTL on cache keys — no indefinite caching
- Use
JSON.stringify / JSON.parse for complex values
BullMQ
Queues
- Define queues in
lib/queue/queues.ts — one queue per job domain
- Use the shared Redis connection from
lib/queue/client.ts
- Name queues in kebab-case:
email-notifications, data-processing
Jobs
- Jobs call services — no business logic in the job processor itself
- Keep job payloads small and serializable — pass IDs, not full objects
- Set sensible defaults:
attempts: 3, backoff: { type: "exponential", delay: 1000 }
Workers
- Define workers in
lib/queue/workers.ts
- Workers run in the same process during development
- For production, consider a separate worker entry point
Testing
- Use Vitest as the test runner
- Test services independently with unit tests (mock db/redis)
- Test route handlers with integration tests using
next/test or direct fetch
- Test server actions by calling them directly in tests
- Use
drizzle-kit test utilities or a test database for integration tests
- Factory functions for test data (not fixtures) — keep in
tests/factories/
Development Environment
- All commands run via
make (Docker Compose under the hood) — never run npm directly
- Use
/nextjs-fullstack-bootstrap when setting up a new project from scratch
- Services: app (Next.js), db (Postgres), redis
- Hot reload via
WATCHPACK_POLLING=true inside Docker
node_modules lives inside the container (anonymous volume)
- Database URL via environment variable:
DATABASE_URL=postgres://...