Node + TypeScript REST API Rules
Layered-architecture rules for Node REST APIs with Zod input validation, a single error envelope, and structured logging.
Use when: Use in Express/Fastify/Hono TypeScript backends where you want strict router-service-repository layering and consistent error responses.
The skill file
Cursor · .cursorrules.cursorrules2901 chars
You are working on a Node.js REST API written in strict TypeScript.
## Layered architecture
- Three layers, strict downward dependencies: `routes/` → `services/` → `repositories/`. A route never touches the database; a repository never knows about HTTP.
- Routes: parse and validate input, call exactly one service function, map the result to a response. Max ~20 lines per handler — if it grows, the logic belongs in a service.
- Services: business logic only. They accept plain typed objects (never `req`/`res`), return domain objects, and throw typed domain errors (`NotFoundError`, `ConflictError`, `ForbiddenError`).
- Repositories: one per aggregate (`user.repository.ts`). All SQL/ORM calls live here. Return domain types, not ORM row types.
- No `utils.ts` dumping ground. Helpers get a named module per concern (`pagination.ts`, `slug.ts`).
## Validation
- Every endpoint validates `body`, `query`, and `params` with Zod schemas before any logic runs. Unvalidated `req.body` must never reach a service.
- Schemas live next to the route in `<resource>.schemas.ts` and export both schema and `z.infer` type: `export type CreateUserInput = z.infer<typeof createUserSchema>`.
- Use `.strict()` on body schemas so unknown keys are rejected, and `z.coerce.number()` for numeric query params.
- Validate environment variables once at startup in `env.ts` with Zod; crash on failure. No `process.env.X` anywhere else.
## Error envelope
- Every non-2xx response uses one shape: `{ "error": { "code": "RESOURCE_NOT_FOUND", "message": "...", "details": [] } }`. `code` is SCREAMING_SNAKE and machine-stable; `message` is human-readable and safe to show users.
- One central error-handling middleware maps domain errors to status codes (NotFound→404, Conflict→409, validation→422 with Zod issues in `details`). Handlers never call `res.status(500)` themselves.
- Never leak internals: no stack traces, SQL, or library error messages in responses. Log them instead.
- Throw early. No `return null` from services to signal "not found" — throw `NotFoundError` and let the middleware translate it.
## Logging
- Structured JSON logs via pino. No `console.log` anywhere — lint forbids it.
- Attach a `requestId` (UUID, from incoming `x-request-id` or generated) to every log line via a child logger created in middleware.
- Log one event per request at completion: method, path, status, duration ms. Log errors with `err` so the serializer captures the stack.
- Never log secrets, tokens, passwords, or full request bodies. Redact `authorization` and `cookie` headers in the logger config.
## Conventions
- Async/await only — no `.then()` chains, no callbacks. Wrap external calls (HTTP, queues) with a timeout.
- Plural kebab-case resource paths (`/api/v1/user-invites`), no verbs in URLs. Verbs belong in the HTTP method.
- Return 201 with the created resource on POST, 204 with empty body on DELETE.
Install
drop into your repoSave this to your project or home directory so Cursor can load it.
path./.cursorrules
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…