nextjs-fullstack-patterns
Skill4 starsChanged 3 months agoMITView on GitHub
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.
In this repository53 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- llms.txtllms.txt
- actionableSkill
- astro-bootstrapSkill
- astro-patternsSkill
- autopilotSkill
- axon-code-intelSkill
- brainstormingSkill
- commit-conventionsSkill
- cross-repo-contextSkill
- django-bootstrapSkill
- django-patternsSkill
- executeSkill
- find-sessionSkill
- flutter-bootstrapSkill
- flutter-patternsSkill
- full-featureSkill
- intelSkill
- nextjs-bootstrapSkill
- nextjs-fullstack-bootstrapSkill
- nextjs-mui-bootstrapSkill
- nextjs-patternsSkill
- nextjs-shadcn-bootstrapSkill
- orchestratorSkill
- phase-breakdownSkill
- plan-and-scopeSkill
- prdSkill
- reactSkill
- recallSkill
- reflectSkill
- relaySkill
- rememberSkill
- scaffoldSkill
- scratchSkill
- tailwindSkill
- ux-specSkill
- ux-to-promptsSkill
- verify-astroSkill
- verify-djangoSkill
- verify-flutterSkill
- verify-nextjs-fullstackSkill
- verify-nextjs-muiSkill
- verify-nextjs-shadcnSkill
- verify-nextjsSkill
- verifySkill
- visual-feedbackSkill
- work-disciplineSkill
- worktreesSkill
- wrapSkill
- write-as-friendSkill
- write-as-humanSkill
- writing-clearlySkill
- writing-skillsSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
36 rules. About 0 are common, 13 are distinctive, and 23 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈13
Distinctive · estimate
This author's own choices. Worth a look.
23
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 58
Every table gets id (serial primary key), uuid (for public-facing IDs), createdAt, updatedAt
- Distinctive: line 59
Use a shared helper for the common columns:
- Very specific: line 66
Use pgTable from drizzle-orm/pg-core
- Distinctive: line 71
Use the Drizzle query builder — not raw SQL
- Very specific: line 72
Use db.select() / db.insert() / db.update() / db.delete() for simple operations
- Very specific: line 73
Use db.query.tableName.findMany() / findFirst() with with for relational queries
- Distinctive: line 74
Always scope queries: filter by tenant/user, never return unscoped data
- Very specific: line 75
Use .$dynamic() for conditional query building
- Distinctive: line 81
Never edit a migration that's been pushed to main
- Very specific: line 82
Run migrations via Makefile: make db-generate, make db-migrate
- Very specific: line 86
Use connection pooling via Pool — not single Client
- Very specific: line 94
Use the Drizzle adapter (@auth/drizzle-adapter) for session/account storage
- Distinctive: line 95
Default to database sessions (not JWT) for fullstack apps
- Very specific: line 98
Server Components: call auth() to get the session, redirect if null
- Very specific: line 99
Route Handlers: call auth() at the top, return 401 if null
- Very specific: line 100
Server Actions: call auth() at the top, throw if null
- Very specific: line 101
Middleware: use auth as middleware in middleware.ts for blanket route protection
- Very specific: line 107
Use auth() — never parse cookies or tokens manually
- Distinctive: line 119
Use Zod schemas for all request validation
- Distinctive: line 136
Always validate input with Zod before processing
- Very specific: line 137
Always check auth via auth() before any data mutation
- Very specific: line 145
Use revalidatePath() or revalidateTag() after mutations to bust caches
- Very specific: line 146
Never redirect inside a try/catch — call redirect() outside it (it throws internally)
- Distinctive: line 160
Test route handlers and server actions as integration tests
- Very specific: line 165
Use for: caching, rate limiting, session storage (if needed beyond Auth.js), BullMQ connection
- Distinctive: line 167
Always set TTL on cache keys — no indefinite caching
- Very specific: line 168
Use JSON.stringify / JSON.parse for complex values
- Very specific: line 174
Use the shared Redis connection from lib/queue/client.ts
- Distinctive: line 179
Keep job payloads small and serializable — pass IDs, not full objects
- Distinctive: line 189
Use Vitest as the test runner
- Distinctive: line 190
Test services independently with unit tests (mock db/redis)
- Very specific: line 191
Test route handlers with integration tests using next/test or direct fetch
- Distinctive: line 192
Test server actions by calling them directly in tests
- Very specific: line 193
Use drizzle-kit test utilities or a test database for integration tests
- Very specific: line 197
All commands run via make (Docker Compose under the hood) — never run npm directly
- Very specific: line 198
Use /nextjs-fullstack-bootstrap when setting up a new project from scratch
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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.tsorapiFetchwrapper; 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 typesDrizzle ORM
Schema
- Define tables in
lib/db/schema/, one file per domain - Every table gets
id(serial primary key),uuid(for public-facing IDs),createdAt,updatedAt - Use a shared helper for the common columns:
export const timestamps = { createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull().$onUpdate(() => new Date()), }; - Use
pgTablefromdrizzle-orm/pg-core - Define relations in the same file as the table using
relations() - Export all tables from
lib/db/schema/index.ts
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()withwithfor 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
dbinstance inlib/db/index.tsusingdrizzle(pool)withnode-postgres - Use connection pooling via
Pool— not singleClient - Export the typed
dbfor 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.tsre-exportshandlers - 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
authas middleware inmiddleware.tsfor 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.tsif 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
- Use Zod schemas for all request validation
- Define schemas alongside the route or in a shared
lib/validators/directory - Return 400 with structured error response on validation failure:
{ error: "Validation failed", details: z.flattenError(zodError).fieldErrors }
Response Shape
- Collections use the canonical paginated envelope:
{ page, count, num_pages, results: T[] }withpage/page_sizequery 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
- Return a result object, not void:
type ActionResult<T = void> = { success: true; data: T } | { success: false; error: string }; - Use
revalidatePath()orrevalidateTag()after mutations to bust caches - Never redirect inside a try/catch — call
redirect()outside it (it throws internally)
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/headersor Next.js request APIs — they receive data as arguments
Testing
- Services are the primary unit-test target — test business logic without HTTP
- Mock the
dbandredisclients, not the service functions themselves - Test route handlers and server actions as integration tests
Redis
- Single client in
lib/redis.tsusingioredis - 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.parsefor 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/testor direct fetch - Test server actions by calling them directly in tests
- Use
drizzle-kittest 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 runnpmdirectly - Use
/nextjs-fullstack-bootstrapwhen setting up a new project from scratch - Services: app (Next.js), db (Postgres), redis
- Hot reload via
WATCHPACK_POLLING=trueinside Docker node_moduleslives inside the container (anonymous volume)- Database URL via environment variable:
DATABASE_URL=postgres://...
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

