Full-stack Next.js architecture for product engineers. A strict 9-layer pattern covering database schemas (Drizzle), API routes, Axios services, React Query hooks, Zod validation, and shadcn/ui components. Every new feature follows the same flow so the codebase stays consistent as it scales. Use when adding CRUD resources, API routes, React Query hooks, Drizzle schemas, or any full-stack feature to a Next.js app.
install
npx skills add nabinkhair42/nk-skills --skill product-stack
Product Stack
A full-stack Next.js architecture for shipping CRUD-heavy applications fast without sacrificing consistency. Built around Next.js App Router, Drizzle ORM, Axios, React Query, Zod, and shadcn/ui.
The core idea: every feature follows the same 9-layer flow. Database to UI, no shortcuts.
Backend flow: DB Schema -> Route Handlers -> Server Services -> Response Helpers Frontend flow: Endpoints -> Services -> Hooks -> Components
Full code lives in reference/api.md (Layers 1–8) and reference/db.md (Layer 9). Read those when writing code for a layer; this file defines the rules.
Project Structure
src/
├── app/
│ ├── (marketing)/ # Public pages
│ ├── dashboard/ # Protected pages
│ ├── api/ # API route handlers
│ │ └── {resource}/
│ │ ├── route.ts # Collection: GET (list), POST (create)
│ │ └── [id]/route.ts # Item: GET, PUT, DELETE
│ ├── actions/ # Server Actions
│ ├── layout.tsx
│ └── globals.css
│
├── config/
│ ├── axios.ts # Axios instance + interceptors
│ ├── api-endpoints.ts # All API paths + React Query keys
│ ├── constants.ts # Enums, storage keys, feature flags
│ └── query-client.ts # TanStack Query client defaults
│
├── services/
│ ├── {resource}.service.ts # Frontend: Axios calls to API routes
│ └── server/ # Server-only: called from route handlers
│ └── {service}.ts
│
├── hooks/
│ └── use-{resource}.ts # React Query hooks wrapping services
│
├── schemas/
│ └── {resource}.ts # Zod validation + inferred types
│
├── types/
│ └── {resource}.ts # TypeScript interfaces
│
├── components/
│ ├── ui/ # shadcn/ui primitives (never hand-edit)
│ ├── loaders/ # Skeletons, spinners
│ ├── dialogs/ # All modal/dialog components
│ └── {feature}/ # Feature-scoped components
│
├── providers/
│ ├── root-provider.tsx # Composes all providers
│ ├── theme-provider.tsx
│ └── query-provider.tsx # QueryClientProvider setup
│
├── db/
│ ├── index.ts # Drizzle instance + connection
│ ├── schema/
│ │ ├── index.ts # Barrel export
│ │ ├── columns.ts # Reusable column helpers
│ │ ├── auth.ts # User, session, account tables
│ │ └── {resource}.ts # Domain tables with enums + indexes
│ └── migrations/ # Generated by drizzle-kit
│
├── lib/
│ ├── utils.ts # cn() and shared formatters
│ ├── response/
│ │ └── server-response.ts # successResponse, errorResponse, Errors.*
│ └── middleware/
│ └── api-middleware.ts # protectedApi, adminApi wrappers
│
├── drizzle.config.ts # Drizzle Kit configuration (project root)
└── auth.ts # Auth configuration
The 9 Layers
Each layer has one job. Full code per layer is in the reference files.
1. DB Schema — db/schema/{resource}.ts
Drizzle tables with enums, indexes, $inferSelect/$inferInsert types (reference/db.md).
- Every table spreads shared
timestamps - Index every FK column — Drizzle does not auto-index them
onDeletealways explicit (cascadeorset null)- Types derived from the schema, never hand-written
2. Endpoints — config/api-endpoints.ts
All paths + React Query keys in ONE file (reference/api.md).
- Static paths for collections, function paths
(id) => ...for items QUERY_KEYSmirrors endpoint structure- Never construct API paths outside this file
3. Zod Schema — schemas/{resource}.ts
Validation AND types from one schema.
z.infer<typeof schema>for all input types — never manual interfaces- Derive update schemas with
.partial() z.transform()+.pipe()for normalization
4. Route Handlers — app/api/{resource}/route.ts + [id]/route.ts
Thin: validate → call server service → respond via helpers (reference/api.md).
- Wrap with
protectedApi/adminApimiddleware — never raw handlers schema.safeParse()every request body before processing- Return only via
successResponse,paginatedResponse, orErrors.* await ctx.params(Promise in Next.js 15+)
5. Service — services/{resource}.service.ts
Thin axios wrappers. One method per endpoint.
- Returns
response.data; typed withApiResponse<T> - No error handling, no toasts, no side effects
6. Hook — hooks/use-{resource}.ts
React Query wraps services; ALL cache management lives here.
- List hooks accept a params object (
useProjects({ page, limit })) so sort/filter params compose later (see data-table-pattern) - Single-item queries use
enabled: !!id queryKeyalways fromQUERY_KEYSfactory; every param that affects the result goes in the key- Cache strategy is either/or per mutation: default
invalidateQueries; if the flow adopts optimistic-cache-pattern, switch fully tosetQueriesData— never both in oneonSuccess - TanStack Query v5:
isPending, notisLoading, on mutations
7–9. Components — components/{resource}/
Columns file → Table wrapper → Dialogs (create/edit/delete), using the hook.
- Components call hooks, never services
- Loaders/skeletons in
components/loaders/ - For forms follow
form-stack; for tables followdata-table-pattern
Server Actions vs API Routes
If your frontend is the only consumer, prefer Server Actions. If anything else calls it (mobile app, third party, webhooks), use an API route. Example in reference/api.md.
| Use case | Pattern |
|---|---|
| Form submission from your app | Server Action |
| Simple create/update/delete from a dialog | Server Action |
| External API consumed by mobile app or third party | API Route Handler |
| Webhook endpoint | API Route Handler |
| Cacheable GET endpoint | API Route Handler |
| Complex multi-step mutation with streaming | API Route Handler |
Server Actions must still re-validate with the same Zod schema — client validation is UX, server validation is the gate.
Server Components by Default
Fetch data directly in Server Components with Drizzle — do not call your own API routes from the server. Pass initial data to client components as props. Keep "use client" at the leaf. Details in reference/api.md.
Adding a New Feature Checklist
When you need to add a new resource (e.g. “tasks”), follow this exact order:
- DB Schema - Define table in
db/schema/task.tswith enums, indexes, and$inferSelect/$inferInserttypes. Export fromdb/schema/index.ts. Rundrizzle-kit generate+drizzle-kit migrate - Endpoints - Add
TASKStoAPI_ENDPOINTSandQUERY_KEYSinconfig/api-endpoints.ts - Zod Schema - Create
schemas/task.tswith request/form validation + inferred input types - Types - Add interfaces to
types/task.tsif needed beyond Zod and Drizzle inference - Route - Create
app/api/tasks/route.ts(GET list, POST create) andapp/api/tasks/[id]/route.ts(GET, PUT, DELETE) - Service - Create
services/task.service.tswith CRUD methods using axios - Hook - Create
hooks/use-tasks.tswith useQuery/useMutation wrapping the service - Components - Build UI in
components/tasks/using the hook - Loaders - Add skeleton in
components/loaders/for the new views
Never skip a layer. Components never call services directly. Services never show toasts. Hooks never construct URLs. Route handlers never return raw NextResponse.json() without the response helpers.
For spec-driven features, write a spec first with feature-spec.
Tech Stack
- Next.js 15+ (App Router, Server Components, Server Actions) - framework
- TypeScript - strict mode
- PostgreSQL + Drizzle ORM - database + type-safe queries
- shadcn/ui - UI components (never hand-edit
components/ui/) - TanStack React Query v5 - client-side server state management
- Axios - HTTP client with interceptors (for client -> API route calls)
- Zod - schema validation + type inference
- Pill Toaster - toast notifications (
pnpm dlx shadcn@latest add https://toast.nabinkhair.com.np/r/pill-toaster.json; mount<Toaster />in the root provider)
Common Mistakes
- Hardcoded API paths - Always use
API_ENDPOINTS, never/api/projectsas a string in services - Calling services from components - Use hooks. Components should only know about hooks
- Manual TypeScript types for form data - Use
z.infer<typeof schema>, never duplicate types - Toast in services - Services are data-only. Toast belongs in hook
onSuccess/onError - Raw NextResponse.json in routes - Use
successResponse(),Errors.*(),paginatedResponse() - Missing query invalidation - Every mutation must update cache OR invalidate — pick one strategy, never both (see Layer 6 rules)
- Fetching with empty ID - Always use
enabled: !!idon single-item queries - Business logic in components - Put it in server services or route handlers
- Forgetting
.safeParse()- Always validate request body in route handlers before processing - Creating axios instances per service - One instance in
config/axios.ts, import everywhere - Missing FK indexes - Drizzle does not auto-index foreign keys. Add
index()for every FK column or joins will full-scan - Using API routes for simple internal mutations - Use Server Actions when the frontend is the only consumer
"use client"too high in the tree - Keep it as close to the leaf as possible. Server Components reduce client JS by up to 70%- Forgetting
awaitonparams- In Next.js 15+,paramsandsearchParamsin page/route components are Promises - Using
isLoadingfrom mutations - TanStack Query v5 renamed it toisPending