agentleFS
theyoungastronautstheyoungastronauts/polaris/skills/nextjs-fullstack-patterns/SKILL.md

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.

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
  1. Very specific:

    Every table gets id (serial primary key), uuid (for public-facing IDs), createdAt, updatedAt

    line 58
  2. Distinctive:

    Use a shared helper for the common columns:

    line 59
  3. Very specific:

    Use pgTable from drizzle-orm/pg-core

    line 66
  4. Distinctive:

    Use the Drizzle query builder — not raw SQL

    line 71
  5. Very specific:

    Use db.select() / db.insert() / db.update() / db.delete() for simple operations

    line 72
  6. Very specific:

    Use db.query.tableName.findMany() / findFirst() with with for relational queries

    line 73
  7. Distinctive:

    Always scope queries: filter by tenant/user, never return unscoped data

    line 74
  8. Very specific:

    Use .$dynamic() for conditional query building

    line 75
  9. Distinctive:

    Never edit a migration that's been pushed to main

    line 81
  10. Very specific:

    Run migrations via Makefile: make db-generate, make db-migrate

    line 82
  11. Very specific:

    Use connection pooling via Pool — not single Client

    line 86
  12. Very specific:

    Use the Drizzle adapter (@auth/drizzle-adapter) for session/account storage

    line 94
  13. Distinctive:

    Default to database sessions (not JWT) for fullstack apps

    line 95
  14. Very specific:

    Server Components: call auth() to get the session, redirect if null

    line 98
  15. Very specific:

    Route Handlers: call auth() at the top, return 401 if null

    line 99
  16. Very specific:

    Server Actions: call auth() at the top, throw if null

    line 100
  17. Very specific:

    Middleware: use auth as middleware in middleware.ts for blanket route protection

    line 101
  18. Very specific:

    Use auth() — never parse cookies or tokens manually

    line 107
  19. Distinctive:

    Use Zod schemas for all request validation

    line 119
  20. Distinctive:

    Always validate input with Zod before processing

    line 136
  21. Very specific:

    Always check auth via auth() before any data mutation

    line 137
  22. Very specific:

    Use revalidatePath() or revalidateTag() after mutations to bust caches

    line 145
  23. Very specific:

    Never redirect inside a try/catch — call redirect() outside it (it throws internally)

    line 146
  24. Distinctive:

    Test route handlers and server actions as integration tests

    line 160
  25. Very specific:

    Use for: caching, rate limiting, session storage (if needed beyond Auth.js), BullMQ connection

    line 165
  26. Distinctive:

    Always set TTL on cache keys — no indefinite caching

    line 167
  27. Very specific:

    Use JSON.stringify / JSON.parse for complex values

    line 168
  28. Very specific:

    Use the shared Redis connection from lib/queue/client.ts

    line 174
  29. Distinctive:

    Keep job payloads small and serializable — pass IDs, not full objects

    line 179
  30. Distinctive:

    Use Vitest as the test runner

    line 189
  31. Distinctive:

    Test services independently with unit tests (mock db/redis)

    line 190
  32. Very specific:

    Test route handlers with integration tests using next/test or direct fetch

    line 191
  33. Distinctive:

    Test server actions by calling them directly in tests

    line 192
  34. Very specific:

    Use drizzle-kit test utilities or a test database for integration tests

    line 193
  35. Very specific:

    All commands run via make (Docker Compose under the hood) — never run npm directly

    line 197
  36. Very specific:

    Use /nextjs-fullstack-bootstrap when setting up a new project from scratch

    line 198

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

  • 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 pgTable from drizzle-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() 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

  • 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[] } 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

  • Return a result object, not void:
    type ActionResult<T = void> = { success: true; data: T } | { success: false; error: string };
  • Use revalidatePath() or revalidateTag() 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/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://...

Download now (ZIP)

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.