Controversial claim. Nest.js is the best backend framework I have shipped production code with. I have shipped on PHP frameworks, on Python frameworks, on most of the recent Node options. The closest contender is FastAPI. FastAPI is not in the same room once the codebase has a few teams and a few years on it.

Here is the evidence.

A Cursor-generated pull request lands in our review queue on a Tuesday morning. It compiles. The tests pass. The endpoint returns the right shape. The controller is running its own database query.

Two files away, another controller is doing the same thing for a different entity. A service class exists for the first one. Nobody reached for it.

That PR is fine for a hobby project and slow poison for a production codebase. By sprint three the modules sprawl, validation drifts, and every service invents its own exception class. The diff that looks fine in isolation becomes the reason the next senior hire wants to rewrite everything. Frameworks that bake those decisions in stop that drift. Frameworks that leave them optional do not.

That is the position we are defending in this manifesto. It is the published standard our small engineering team follows so every new service, every AI-generated pull request, and every new hire starts from the same baseline. It is not a tutorial. It is the rules we ship by.

We still evaluate lighter alternatives every quarter. None have displaced Nest.js for the kind of systems we maintain.

Here is what the landscape looks like.

framework comparison✓ built-in   ~ partial   ✗ missing
Framework DI Async-first Strict types DTO validation Module enforcement Cold start
Express~excellent
Hapi~~good
AdonisJS~good
Spring Bootslow
Django REST~~~good
FastAPI~excellent
Nest.jsgood

FastAPI follows the closest parallel in Python. It uses decorators for routing, Pydantic models for validation and OpenAPI, and Depends for explicit dependency injection. The approach works, and we have shipped Python services with it that we are proud of.

FastAPI stays lighter by design. It lacks a formal module system, a deep DI container, CLI scaffolding for enterprise patterns, and built-in opinions on microservices or GraphQL. We respect that choice. Our manifesto simply fills the gaps we need for long-lived TypeScript services.

· · ·

Why the manifesto exists

A published set of rules turns both humans and AI agents into consistent contributors. Without it, Cursor and Claude Code produce working code that drifts after three sprints: flat modules, duplicated guards, invented error classes, inconsistent DTOs. We keep the manifesto at the repo root. Agents read it first. Reviews become faster because the shape is already correct.

The document contains eleven sections with full rules. Six more exist only as titles for now. That honesty matters. We ship software, not perfect documentation.

· · ·

Architecture

We treat the application as a set of bounded contexts. Each context becomes a module. Shared logic lives in libraries, never in a common module that everything imports. This prevents the ball-of-mud pattern that appears when services grow past ten modules. The structure stays obvious six months later, which is the whole point.

Structure

Standard folder layout per module: controllers, services, entities, dto, guards, interceptors, tests. No surprises. Cross-module communication happens through exported services or events, never direct imports of internal files. The Nest CLI generates the skeleton. We enforce it.

src/
├── modules/
│   ├── auth/
│   ├── user/
│   ├── order/
│   ├── payment/
│   ├── shared/
│   │   ├── decorators/
│   │   ├── validators/
│   │   ├── middlewares/
│   │   ├── classes/
│   │   └── shared.module.ts
├── app.module.ts
└── main.ts

View the reference module on GitHub →

Modules and services

Modules declare their providers, controllers, and imports explicitly. We use dynamic modules only when configuration demands it. Services stay single-responsibility. If a service grows beyond two or three public methods, we split it. This rule has saved us more than once during refactoring.

Bad on the left, good on the right.

✗ Bad   user.controller.ts
@Get()
async getUsers() {
  const users = await this.repo.find();
  return users.filter(u => u.active);
}
✓ Good   user.controller.ts
@Get()
getUsers() {
  return this.userService.getActiveUsers();
}

Coding standards

Strict TypeScript, no any, explicit return types, small functions. We run ESLint with the full typescript-eslint ruleset. Violations break the build. Naming follows the framework conventions so a new developer can guess the file location from the class name alone.

{
  "compilerOptions": {
    "strict": true
  }
}

Every controller input goes through a DTO using class-validator and class-transformer. The global ValidationPipe runs with whitelist: true and forbidNonWhitelisted: true so the wrong shape never reaches a service.

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;
}

Enums replace magic strings. The cost of the rule is zero; the cost of skipping it shows up in the next bug report.

export enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
}

Use async / await instead of .then() chains.

Hard to follow on the left; cheap to read on the right.

✗ Bad   user.service.ts
getActiveUsers() {
  return this.repo.find()
    .then(users => users.filter(u => u.active))
    .then(active => active.map(u => this.toDto(u)));
}
✓ Good   user.service.ts
async getActiveUsers() {
  const users = await this.repo.find();
  return users
    .filter(u => u.active)
    .map(u => this.toDto(u));
}

The mental model is closer to synchronous code, the stack traces stay readable, and refactoring stops involving twelve indentation levels.

Imports group in a fixed order, separated by a blank line:

NestJS
Third-party
Local

REST

Resource-oriented routes. Versioning via URL prefix. Request and response bodies use validated DTOs. No business logic in controllers. Every endpoint returns a consistent shape or throws a handled exception.

@Post()
createUser(@Body() dto: CreateUserDto) {}

@Get()
getUsers(@Query() query: PaginationDto) {}

@Put(':id')
replaceUser(@Param('id') id: string) {}

@Patch(':id')
updateUser(@Param('id') id: string) {}

@Delete(':id')
deleteUser(@Param('id') id: string) {}

HTTP codes

We use standard codes and nothing else. 200 for success with body, 201 for creation, 204 for no content. Client errors stay in the 4xx range with meaningful messages. The framework exception filter translates domain errors to the right status. We do not invent new ones.

@HttpCode(HttpStatus.CREATED)
@Post()
createUser(@Body() dto: CreateUserDto) {}

@HttpCode(HttpStatus.NO_CONTENT)
@Delete(':id')
removeUser(@Param('id') id: string) {}

Errors

Centralized exception filter catches everything. Domain exceptions carry enough context for logging and user messages. We never leak stack traces in production. AI agents sometimes invent new exception classes when they could have used NotFoundException or ConflictException. The filter rule, plus a brief code review, stops that immediately.

The same endpoint, before and after the rule lands in code review.

✗ Bad   user.controller.ts
@Get(':id')
async getUser(@Param('id') id: string) {
  try {
    const user = await this.repo.findOne({ where: { id } });
    if (!user) return { error: 'not found' };
    return user;
  } catch (e) {
    return { error: 'something went wrong' };
  }
}
✓ Good   user.controller.ts
@Get(':id')
async getUser(@Param('id') id: string) {
  const user = await this.userService.findById(id);
  if (!user) throw new NotFoundException();
  return user;
}
@Catch(HttpException)
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    response.status(exception.getStatus()).json({
      statusCode: exception.getStatus(),
      message: exception.message,
    });
  }
}

Security

Helmet, rate limiting, and CORS are configured once at the application level. Input validation happens before the controller through a global ValidationPipe with whitelist and forbidNonWhitelisted enabled. Auth guards run early. Secrets come from the environment or ConfigModule.

.env files never end up in Git. No exceptions.
bash
cp .env.example .env

Auth

JWT as the default bearer token. Guards protect routes. We support refresh tokens via HttpOnly cookies when needed. Roles and policies live in custom decorators that compose cleanly with existing guards. The auth check is centralized; we never duplicate it inside controllers.

consumer.apply(AuthMiddleware).forRoutes('*');
app.useGlobalGuards(rolesGuard);

Performance

We profile with clinic.js or OpenTelemetry. Response compression and caching live in interceptors where appropriate. Database queries stay inside services and use the repository pattern.

We avoid N+1 by design, not by heroic last-minute fixes.

Pagination is required on every list endpoint; defaults are 20, max is 100.

The classic N+1 on the left. The same data on the right, in one query.

✗ Bad   orders.service.ts
async getOrdersWithItems(userId: string) {
  const orders = await this.ordersRepo.find({ where: { userId } });
  for (const order of orders) {
    order.items = await this.itemsRepo.find({ where: { orderId: order.id } });
  }
  return orders;
}
✓ Good   orders.service.ts
async getOrdersWithItems(userId: string) {
  return this.ordersRepo.find({
    where: { userId },
    relations: { items: true },
  });
}
app.use(compression());

Testing

Unit tests for services, e2e tests for modules. We mock external dependencies with @nestjs/testing and test guards and pipes in isolation. Coverage thresholds are enforced. Tests follow the same module boundaries as production code, which means refactoring a feature module rarely breaks tests outside it.

describe('UserService', () => {
  let service: UserService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [UserService],
    }).compile();

    service = module.get<UserService>(UserService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});
· · ·

The six sections still in TOC only

These are the rules we know are coming but have not yet written down with the same care as the rules above. We are publishing them as one-paragraph stubs rather than letting them sit as empty headers in a Notion file forever.

GraphQL. We use code-first with the Apollo driver. Resolvers live inside their owning modules and reuse the same DTOs and guards as REST. Federation only when business boundaries require it.

Code Quality. ESLint with the full typescript-eslint ruleset on every PR, commitlint enforcing conventional commits, PR size soft-capped at 400 changed lines. The Coding Standards section above is the single source of truth that both humans and agents follow.

DevOps. Multi-stage Docker, OpenTelemetry, graceful shutdown, structured logs. Production configuration lives in environment variables and runtime secret stores, never in committed files.

Database. TypeORM remains the default for its decorator alignment with the rest of the framework. We require explicit repositories and review all migrations by hand. Prisma or Drizzle are allowed when the team proves better fit for the workload, but the bar is real evidence.

Microservices. We prefer modules in a monorepo until decoupling is justified by team boundaries or independent deploy cadence. When we split, each service follows the full manifesto and shares typed contracts.

AWS. Default to ECS Fargate for sustained load. Lambda only for short tasks via the official adapter. All SDK calls wrapped in infrastructure modules with retry and observability.

Framework comparisons in practice

Express plus custom layers still wins for tiny scripts or when absolute minimalism matters. Once you add DI, guards, and consistent structure to an Express codebase, the result starts looking like a lightweight Nest.js anyway. We have been there.

Hapi offers strong plugin and validation features but carries more configuration boilerplate. AdonisJS delivers full MVC productivity if you want templates and conventions closer to Rails. Spring Boot feels familiar to anyone who knows Nest.js; the JVM trade-off shows in cold starts and memory.

Django REST Framework feels mature in the same way the strong PHP frameworks do: opinionated, well-regulated, still shipping serious products at scale. It taught most of us the patterns we now take for granted (serializers, viewsets, permissions, throttling) and for a Python team it remains one of the best backend stories in the industry. The trade-off versus Nest.js is async as opt-in rather than default and types as a team discipline rather than a framework guarantee. Same broad strengths, different decade of design.

FastAPI matches the decorator and validation philosophy almost exactly. Its simplicity is a feature, not a bug. Our needs for deeper module organization and framework-enforced consistency pushed us to Nest.js and keep us here.

Where Nest.js sits in 2026

Nest.js has over 75,000 GitHub stars and roughly 10 million weekly downloads of @nestjs/core, with the curve still climbing. It is not the hottest new thing. That is the point. It is the framework serious teams choose when they expect to maintain the code for years.

TypeORM remains common in Nest.js projects for its natural fit with the decorator model. Newer services often evaluate Prisma for developer experience or Drizzle for query control. We accept all three as legitimate choices, with TypeORM as the default until a project earns the switch.

What AI coding agents do to Nest.js (and what the manifesto stops)

Modern agents generate decent Nest.js code out of the box. They understand decorators, modules, and DTOs. The patterns are popular enough that the training data is dense.

Without guardrails, the same agents produce specific failure modes we have learned to spot on sight:

  • God modules. Everything imported from a single common/ folder. By the time you notice, every change ripples.
  • Bypassed exception filter. A try/catch in a controller returning an ad-hoc response shape. The global filter sits there unused.
  • Scattered validation. One endpoint uses a DTO with class-validator, the next uses a plain interface, the third does manual checks inside the service.
  • Invented exception classes. FooNotFoundException extending Error instead of using NotFoundException. The status code shape drifts a little. The logging context drifts more.
  • Direct repository access in controllers. The service exists. The agent skipped it because the test fixture was easier to write without it.
  • Slow-growing god services. A service starts at three public methods and reaches eleven by the time anyone notices.
These are not exotic failures. They are the predictable shapes of code an agent produces when it has no opinion about where things go.

The manifesto encodes the opinion. Drop it in the repo and the generated code stays in shape, because the path of least resistance for the agent now matches the path of least resistance the framework already wants.

Dry observations from the trenches

The official docs still feature the cats controller. We have all seen the joke slide in conference talks. The line "you do not need modules until you do" usually appears right before someone shares a screenshot of a forty-module monolith that suddenly became maintainable. These are not profound insights. They are reminders that structure pays rent later.

We do not need another framework-of-the-month discussion. The question is whether your team will still understand the service six months after the original author leaves or the AI finishes its sprint.

· · ·

Drop this into your AI coding agent

The manifesto reads well to humans. We also wrote a companion kit that makes Claude Code, Cursor, and Codex follow it without a person in the loop. Four files plus a README, sized to drop into a repository as-is.

CLAUDE.md is Claude Code's project memory file. It sits at the repo root and gets loaded into every Claude Code session for the project automatically.

.claude/skills/nestjs-2muchcoffee/SKILL.md is a formal Claude Code skill with YAML frontmatter. Triggered by description match when Claude is doing Nest.js work, useful if you want the skill to apply across every project on your machine instead of just one repo.

.cursor/rules/nestjs.mdc is Cursor's rules format. We set alwaysApply: true and scope it to TypeScript files in the repo. Cursor reads it on every prompt.

AGENTS.md is the cross-tool standard for Codex CLI, Aider, Continue, and any agent that follows the AGENTS.md convention. Sits at the repo root, read on agent startup.

The kit lives at 2muchcoffeecom/nestjs-manifesto on GitHub. The README explains where each file goes, why we shaped each one the way we did, and how to customize the rules for your team. Fork it, edit it, ship it. Attribution appreciated but not required.

The point of the kit is the same as the point of the manifesto. New code should look like it belongs here. Reviews focus on business logic, not architecture debates. Both human developers and AI agents work faster inside clear lines.

· · ·

Closing

This manifesto is not complete. Six sections remain stubs, and the kit will keep evolving as the agents do. We will write the missing sections when a real project demands the detail. Until then the existing rules keep us shipping without drama.

We publish this because hiding standards never helped anyone. If your team values long-term maintainability over minimal initial ceremony, Nest.js plus a living manifesto remains the most practical choice in 2026.

Which of these rules would your team push back on first?

If you want a second pair of eyes on a Nest.js codebase before it scales, 2muchcoffee can help through the AI development page or the contact form.