EEPNext.js Template
Architecture

Overview

Where things live, how they interact and the "three-layer system"

Architecture diagram

Where things live

src/ - application code

Tests live in a __tests__/ subfolder co-located with the code they test - this applies across components/, hooks/, store/, utils/, and validation/. See Testing for the full convention.

LocationWhat lives hereRule
app/Next.js file-convention files onlypage.tsx, layout.tsx, error.tsx, not-found.tsx etc. - thin shells that delegate to components/ and classes/. No business logic.
app/(app)/Authenticated app pagesAll pages that require a signed-in user. Layout mounts the auth gate and navbar automatically.
app/(auth)/Auth pagesSign-in and sign-up. Bare layout - no navbar, no auth gate.
app/restricted/Role-based access denialShown when a signed-in user lacks a qualifying role (Admin or SuperAdmin). Lives outside (app)/ so it can be reached without passing the role check again.
app/api/Route handlersReads + external writes; auth enforced in-handler (withAuth/withRole); Zod-validated at the boundary
actions/Server actionsUI writes; share the same Zod schema and role gate as the route
components/ui/shadcn primitivesOwned by you - edit freely to match your design system
components/common/Shared UI componentsReusable across features; never import from classes/
components/features/Feature-specific componentsScoped to one feature; never import from classes/
components/providers/Client-side React context providersWrappers that make a service available to the whole component tree via React context (e.g. SWR config, theme). Composed here, mounted once in layout-client.tsx
hooks/SWR data-fetching hooksOne file per resource (use-*.ts); no useEffect fetching
store/Zustand storesOne file per domain; co-locate types
classes/services/<domain>/Business logic + integrationsserver-only; one folder per domain
classes/errors/Shared error typesNot server-only - thrown from both layers
classes/loggers/Logger classesserver-only - Winston writes to the filesystem (Node.js only)
validation/Zod schemasOne file per domain; shared between route and action
types/Shared TypeScript typesPull up only when consumed across unrelated layers
lib/Third-party instantiationConfig and init only - no business logic
config/constants/App-wide constantsOnly for values shared across multiple modules
utils/Shared utility functionsPure functions
db/Drizzle schema, pool, migrationsSchema is the source of truth; types inferred from it
proxy.tsNext.js middleware (Clerk auth gate)Lightweight checks only - redirects and session reads

Project root - tooling and infrastructure

LocationWhat lives here
scripts/DB seeding, smoke checks, asset copy scripts. Run outside the Next.js context via tsx.
e2e-tests/Playwright end-to-end tests. One spec per feature.
contract-tests/Live network checks that a third-party API still returns the shape we depend on. Run with pnpm test:contract.
drizzle/Generated SQL migrations and Drizzle metadata. Committed - never edit by hand.
docs/This documentation site (Fumadocs). Separate Next.js app, runs on :3001.
diagrams/Architecture diagram source (main.py). Regenerate with cd diagrams && uv run python main.py.
eslint-rules/Custom ESLint rules specific to this project.
public/Static assets served at /. Includes vendored Swagger UI bundle.
.github/GitHub Actions CI workflow (workflows/ci.yml) and the pull request template.
.devcontainer/Dev Container config for a zero-setup VS Code / Codespaces environment.
.husky/Git hooks. The pre-commit hook runs lint, typecheck, tests, and build.

The three layers

The template is built on three strict layers, which foster clear separation of concerns and code reusability! Each layer has one job and one place to house the code. Business logic never bleeds into route handlers, data access never appears in components.

Client - components/, hooks/, store/

Everything the browser runs.

  • All data fetching goes through SWR hooks in hooks/
  • All client state goes through Zustand stores in store/
  • Components are purely presentational, they receive data and call handlers

Server - classes/

All business logic lives here, and only here.

  • Services in classes/services/<domain>/ are server-only, they can never be imported by client code
  • Services own all data-access logic (Drizzle queries, third-party API calls)
  • Services expose a small typed surface to the API layer

API - app/api/, actions/

The boundary between the browser and the server.

  • Route handlers own reads and external writes
  • Route handlers and actions call service methods, they never query the database directly
  • Server actions own UI writes (POST/mutations from our own UI). See Read vs Write for the full breakdown of why.
  • Both surfaces validate all input with Zod schemas before anything reaches the service layer

On this page