Comprehensive Rules for building, testing, and operating scalable Node.js + Express APIs written in TypeScript.
Tired of battling TypeScript errors at 2 AM? Drowning in Express middleware chaos? Wrestling with database migrations that break everything? You're not alone – and there's a better way.
Building production-ready Node.js APIs shouldn't feel like solving a puzzle with missing pieces. Yet most developers are stuck with:
any, the other half crashes with runtime type errorsThese aren't just annoyances – they're productivity killers that turn simple API changes into week-long debugging marathons.
This isn't another generic configuration file. These rules transform your Cursor IDE into an expert pair programmer that knows exactly how to build bulletproof Node.js APIs. Every suggestion, every auto-completion, every refactoring follows battle-tested patterns from production systems handling millions of requests.
Your development experience becomes:
any types// Before: Runtime explosions waiting to happen
app.get('/users/:id', (req, res) => {
const user = await getUserById(req.params.id); // any type, no validation
res.json(user); // could be undefined, null, or error
});
// After: Bulletproof with full IntelliSense
app.get('/users/:id',
param('id').isUUID(),
validate,
async (req: Request<{ id: string }>, res: Response) => {
const user = await userService.findById(req.params.id);
if (!user) throw new AppError('USER_NOT_FOUND', 404);
res.status(200).json(user); // TypeScript knows exactly what this is
}
);
No more scattered try-catch blocks or mysterious 500 errors. Get consistent, debuggable error responses:
{
"timestamp": "2023-10-10T10:10:10Z",
"path": "/v1/users/42",
"code": "USER_NOT_FOUND",
"message": "User not found",
"traceId": "abc123"
}
Schema-first development with Prisma means your database changes are versioned, reviewable, and rollback-safe. No more "it worked in dev" disasters.
Before: 2-3 days to add a new endpoint (fighting types, debugging errors, fixing tests) After: 30 minutes with full confidence it works in production
Your IDE now knows:
Before: SSH into servers, grep through logs, pray you can reproduce locally After: Structured logs with trace IDs, metrics dashboards, errors that tell you exactly what went wrong
Before: Code reviews focused on style, missed business logic bugs After: Reviews focus on architecture, TypeScript catches the rest
Copy the rules into your .cursor/cursor-rules file in your project root.
src/
├─ api/v1/users/
│ ├─ users.route.ts # Express routes
│ ├─ users.controller.ts # Request/response handling
│ ├─ users.service.ts # Business logic
│ ├─ users.validator.ts # Input validation
│ └─ users.type.ts # TypeScript interfaces
├─ common/
│ ├─ errors/
│ ├─ middlewares/
│ └─ utils/
└─ index.ts
Update your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"module": "esnext"
}
}
Your API development transforms from a constant battle against TypeScript and Express into a smooth, predictable workflow where your tools actually help you build better software faster.
Stop accepting that backend development has to be painful. These rules turn Cursor into your expert pair programmer who knows exactly how to build APIs that scale, perform, and stay maintainable as your team grows.
Ready to write APIs the way they should be written? Your future debugging sessions will thank you.
You are an expert in Node.js (≥18 LTS), Express 4+, TypeScript 5+, Prisma 5+, PostgreSQL, Docker, Kubernetes, PM2, Jest, ESLint, Prettier.
Key Principles
- Type-safety first: no `any`, always prefer explicit, self-documenting types; rely on inference only when it is 100 % clear.
- Functional, declarative style; keep functions pure where possible; avoid shared mutable state.
- Keep the happy path obvious; fail fast with early returns.
- Thin controllers, fat services: route handlers only orchestrate, business logic lives in services.
- Micro-service oriented; every service owns its data and communicates over HTTP/JSON or GraphQL.
- Prefer horizontal scaling (Docker + K8s, PM2 cluster) over vertical.
- Everything is code: infrastructure (IaC), migrations, CI/CD pipelines.
- One concern per module; DRY; code must be formatted by Prettier and checked by ESLint before merge.
TypeScript Rules
- `strict: true` in tsconfig; enable `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`.
- Use ESM (`"module": "esnext"`) and top-level `await`.
- Declare return types for every exported function & method.
- Use interfaces for DTOs and Prisma models; use `type` aliases for unions/intersections.
- Prefer generics over `unknown`; never suppress errors with `// @ts-ignore`.
- File naming: `kebab-case.ts`; tests `*.spec.ts`; types `*.type.ts`.
- Directory convention:
src/
├─ api/
│ └─ v1/
│ └─ users/
│ ├─ users.route.ts // Express Router
│ ├─ users.controller.ts
│ ├─ users.service.ts
│ ├─ users.validator.ts
│ └─ users.type.ts
├─ prisma/
├─ common/ // utilities, middlewares, errors
└─ index.ts // app entry
Node.js & Async Patterns
- Always use `async/await`; never nest callbacks.
- Wrap async route handlers with an `asyncHandler` helper to forward errors to `next()`.
- Do not block the event loop; off-load CPU heavy tasks to worker threads or external services.
Error Handling & Validation
- Centralised error middleware; registered after all routes.
- Custom `AppError extends Error` with `statusCode`, `code`, `details`.
- Standard error JSON response:
{
"timestamp": "2023-10-10T10:10:10Z",
"path": "/v1/users/42",
"code": "USER_NOT_FOUND",
"message": "User not found"
}
- Validate and sanitise all input with `express-validator` or `zod` schemas *before* controller logic.
- Log the stack trace server-side, never expose it to clients.
Express Framework Rules
- One Express `Router` per bounded context; mount under versioned path (`/v1/...`).
- All routers are imported and mounted in `src/index.ts`; keep a single Express instance.
- Use `helmet`, `cors`, `compression`, and `express-rate-limit` globally.
- Use `morgan` or `pino-http` for request logging; include `traceId` from OpenTelemetry.
- Always export the initialized router (`export const usersRouter = Router()`) – no side effects.
- Prefer `res.json()` over `res.send()`; always set explicit HTTP status codes.
Prisma ORM Rules
- Schema-first: define models inside `schema.prisma`; never mutate DB outside migrations.
- Run `prisma migrate dev` during local dev; `prisma migrate deploy` in CI.
- Encapsulate Prisma client in a singleton `PrismaService` to avoid connection storms.
- Catch `PrismaClientKnownRequestError` and map to `AppError` with business-level codes.
- Soft delete via `deletedAt` + compound index when needed; reflect in model & queries.
Testing
- Use Jest + supertest; one test file per route.
- Spin up a disposable Postgres with Testcontainers; never hit production data.
- Aim ≥90 % line & branch coverage for critical modules.
- Use `ts-jest` with `isolatedModules: true` for fast feedback.
- Stub external HTTP calls with `nock`.
Performance & Scaling
- Default Node process listens on `0.0.0.0` & port from `PORT` env.
- Run PM2 in cluster mode: `pm2 start dist/index.js -i max`.
- Add `uv_threadpool_size = 64` only when you have many fs/crypto operations.
- Build minimal, multi-stage Docker image:
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json .
RUN npm ci --omit=dev
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
- Kubernetes: add `readinessProbe` hitting `/health`, `livenessProbe` hitting `/live`.
Security
- Store secrets in env vars (Kubernetes Secrets or Vault), never commit to VCS.
- Enforce HTTPS & HSTS at ingress; redirect HTTP → HTTPS in Express.
- Use JWT with RS256; verify in auth middleware; rotate keys.
- Sanitize headers (`helmet`), limit payload size (`express.json({limit:'1mb'})`).
- Keep dependencies patched via `npm audit` in CI.
Observability
- Structured logging with `pino`; logs must include `level`, `time`, `msg`, `traceId`.
- Emit OpenTelemetry traces & metrics; scrape with Prometheus; visualise in Grafana.
- Alert on p95 latency, error rate, and memory usage.
CI/CD
- Mandatory jobs: `lint`, `test`, `build`, `docker-build`, `deploy`.
- Block merge if coverage < target or ESLint errors exist.
Common Pitfalls
- Forgetting `await` inside Express handlers → unhandled promise rejection; ESLint plugin `@typescript-eslint/no-floating-promises` must be enabled.
- Creating multiple PrismaClient instances (e.g., each Lambda invocation) → exhausts DB connections; always reuse singleton.
- Mutating `req.body` after validation; treat inputs as immutable.
Example Route (Users)
```
// users.route.ts
import { Router } from 'express';
import { getUserById } from './users.controller';
import { param } from 'express-validator';
import { validate } from '../../common/middlewares/validate';
export const usersRouter = Router();
usersRouter.get(
'/:id',
param('id').isUUID(),
validate,
getUserById,
);
```
```
// users.controller.ts
import { Request, Response, NextFunction } from 'express';
import { userService } from './users.service';
import { AppError } from '../../common/errors/app-error';
export const getUserById = async (
req: Request<{ id: string }>,
res: Response,
next: NextFunction,
) => {
try {
const user = await userService.findById(req.params.id);
if (!user) throw new AppError('USER_NOT_FOUND', 404);
res.status(200).json(user);
} catch (err) {
next(err);
}
};
```
---
Follow these rules to ensure every Node.js + Express service remains robust, secure, and easy to scale.