Main Our Publications
Designing Reliable REST APIs with NestJS

Designing Reliable REST APIs with NestJS

  • NestJS REST API
  • API design
  • DTO validation
  • pagination
  • idempotency
  • error contract
  • API versioning
  • OpenAPI
Designing Reliable REST APIs with NestJS

Design NestJS REST APIs with durable resource semantics, bounded validation, deterministic pagination, idempotency, concurrency control, safe errors, compatibility policy, and verified OpenAPI.

Designing Reliable REST APIs with NestJS

A reliable API is predictable when clients retry, send invalid input, act concurrently, upgrade at different times, or encounter a partial failure. Its resource names are the visible part. The deeper contract includes method semantics, authorization, validation, ordering, time and money representation, error codes, replay behavior, compatibility, and operational evidence.

NestJS gives teams a coherent way to express that contract through controllers, pipes, guards, interceptors, exception filters, modules, and OpenAPI integration. Those mechanisms do not decide where a transaction begins, which service owns a fact, whether a command may be repeated, or which change will break a consumer. Reliability comes from making those decisions explicit and applying them consistently.

Start from consumer workflows and domain invariants. For each endpoint, name the actor, resource or command, permitted state, authority check, input limits, atomic change, response, observable side effects, retry rule, and failure modes. Keep transport concerns at the edge and business decisions in application or domain services. Our NestJS development services use this separation so HTTP can evolve without turning every client release into a coordinated emergency.

Model Resources and HTTP Semantics Deliberately

Use stable business nouns for resources: /orders/{orderId}, /customers/{customerId}/addresses, or /invoices/{invoiceId}/payments. Nest a subresource only when the parent relationship changes its identity, authorization, or lifecycle. Deep paths that mirror database joins expose persistence and become brittle when ownership changes.

Not every business action is a field update. Model a cancellation, approval, import, or report generation as a resource when it has its own state, permissions, result, or audit history: POST /orders/{orderId}/cancellations says more than a generic /execute. Keep controllers thin. They parse transport data, invoke policy and use cases, then translate outcomes; they should not coordinate database transactions or reproduce domain rules across routes.

Apply HTTP methods according to their semantics. GET and HEAD must be safe. PUT replaces the state of a known target representation and is idempotent by definition; PATCH applies a documented partial change; DELETE needs an explicit repeat and tombstone policy; POST performs resource-specific processing and is not inherently idempotent. Do not use GET for an action merely because it is convenient to call from a browser.

Choose status codes as part of the contract. A synchronous creation commonly returns 201 with a Location; accepted background work returns 202 plus a status resource; 204 has no response content; 400 describes malformed or policy-invalid input according to your convention; 401 and 403 separate missing identity from denied authority; 404 may intentionally hide a protected resource; 409 represents a state conflict; 412 fits a failed conditional request; and 429 or 503 should explain whether retry is sensible. One consistent policy matters more than arguing over adjacent valid codes endpoint by endpoint.

Validate Every External Boundary

TypeScript types alone do not protect a running API. Validate every untrusted body, path parameter, query value, header, file, webhook, and decoded token claim before it influences a query or business decision. NestJS supports decorated DTO classes through ValidationPipe and runtime schemas through Standard Schema compatible tooling. Choose one clear pattern per boundary and keep its runtime rules visible.

With class DTOs, remember that interfaces, generics, and type-only imports disappear at runtime. A global ValidationPipe can transform values, strip undeclared properties, or reject them with forbidNonWhitelisted. Schema-based validation can infer TypeScript types from the runtime schema and define strict, stripping, or permissive object behavior. In either model, coercion must be deliberate: query strings arrive as strings, and values such as an empty string, an invalid date, or "false" should never become a surprising truthy flag.

Separate create, replacement, patch, query, and response models. Missing and null need different documented meanings. Represent money with an integer minor unit or decimal policy and an explicit currency; represent time with a timezone contract; validate enums and identifiers without guessing. Bound string length, arrays, object depth, file size, page size, date ranges, and batch commands so structurally valid input cannot demand unlimited CPU, memory, or database work.

Validation answers whether input has the expected shape. Authorization answers whether this principal may act on this specific object; domain invariants answer whether the transition is legal now. Never bind request data directly to an ORM entity or spread it into an update. Map allowlisted fields into a command, load authoritative state, enforce object-level policy and invariants, then return an explicit response DTO or read model.

app.useGlobalPipes(new ValidationPipe({
  transform: true,
  whitelist: true,
  forbidNonWhitelisted: true,
  validationError: { target: false, value: false },
}));

export class ListOrdersQuery {
  @IsOptional()
  @IsString()
  @MaxLength(512)
  cursor?: string;

  @Type(() => Number)
  @IsInt()
  @Min(1)
  @Max(100)
  limit = 25;

  @IsOptional()
  @IsEnum(OrderStatus)
  status?: OrderStatus;
}

Make Pagination and Filtering Deterministic

Every collection needs a bounded default and maximum page size plus a total ordering. Offset pagination is simple, supports page numbers, and can be appropriate for modest, relatively stable administrative datasets. Large offsets may force expensive scanning, while concurrent inserts or deletes can shift rows between pages and produce duplicates or omissions.

Cursor pagination suits long or changing feeds when the cursor captures every sort key and a unique tie-breaker, such as (createdAt, id). The next query must reproduce the same filters and lexicographic comparison. Treat the cursor as opaque, version its contents, protect its integrity, and define direction. A cursor that encodes only a non-unique timestamp will eventually skip or repeat records.

Allowlist filter, search, include, field, and sort options. Document operators, case and accent behavior, timezone boundaries, null ordering, and whether repeated filters mean AND or OR. Translate options through an explicit query model instead of accepting column names or arbitrary SQL fragments. Add indexes for measured access patterns and cap expensive date spans, relationship expansion, wildcard search, and exported result size.

A collection response should expose items and reliable navigation metadata. Exact totals are useful for page navigation but can be costly, permission-sensitive, or unstable; offer them only when the product needs them and the query can support them. Test empty pages, deleted cursor targets, equal sort values, concurrent writes, invalid cursors, changes to access rights, and a client resuming after a long pause.

Design Idempotency and Concurrency Together

A network can fail after the server commits a change but before the client receives the response. The client cannot infer whether a payment, order, booking, or job exists, so a reasonable retry can create a duplicate. For retry-sensitive POST commands, accept an idempotency key scoped to the authenticated principal, operation, and often tenant.

Persist the key, a canonical request fingerprint, processing state, and replayable final response in durable storage. Enforce a unique constraint on the scope and key. The first request claims the key; a retry with the same fingerprint receives the stored result; reuse with different input returns a conflict. Define what a concurrent caller sees while processing is in progress, which failures may be retried, how long records live, and whether redacted response data can be safely stored.

The business change and idempotency result must not drift apart. Use one transaction when they share a database, or a workflow with explicit intermediate state and reconciliation when external effects are involved. An in-memory map fails under restarts and horizontal scaling. A cache without durable ownership can expire while the original effect still exists. Downstream providers may also need their own stable key, but their guarantee does not replace yours.

Idempotency prevents duplicate execution of the same command; it does not resolve two different valid commands racing to modify one resource. Prevent lost updates with a version field, strong ETag plus If-Match, an expected-state command, or a database condition in the update. Return 412 for a failed HTTP precondition or a documented 409 conflict, then let the client reload and decide. Domain uniqueness constraints still protect facts such as one active subscription or one external reference.

@Post()
@HttpCode(HttpStatus.CREATED)
async create(
  @CurrentUser() principal: UserPrincipal,
  @Headers('idempotency-key', ParseIdempotencyKeyPipe) key: string,
  @Body() dto: CreateOrderDto,
) {
  return this.idempotency.execute({
    scope: ['create-order', principal.tenantId, principal.userId],
    key,
    request: dto,
    handler: () => this.createOrder.execute(principal, dto),
  });
}

// execute() persists a canonical request fingerprint and result.
// Durable storage enforces one unique record for (scope, key).

Keep Errors Useful, Stable, and Safe

Define one error envelope with a stable machine-readable code, a safe human message, a request or trace identifier, and optional field violations. Clients branch on status and code, never on translated prose. Keep codes at the level of decisions a consumer can make: correct input, authenticate, request access, reload state, wait, retry, or contact support.

Map known domain and dependency failures through central adapters or exception filters, but preserve useful context inside protected telemetry. Distinguish malformed input, failed authentication, denied authorization, hidden or absent resources, current-state conflicts, failed preconditions, rate limits, dependency timeouts, and temporary unavailability. Document retryability and return Retry-After only when it represents a meaningful server estimate or policy.

Do not expose stack traces, SQL, table names, internal hosts, credentials, tokens, raw provider payloads, or sensitive validation values. Authorization failures should not reveal whether another tenant owns an identifier. Log the safe error code, operation, duration, release, dependency category, and correlation data needed to investigate, with sensitive fields redacted and metric labels bounded.

An error contract is operational only when someone sees patterns. Measure rates and latency by endpoint, status family, stable code, dependency, client version, and release. Alert on customer impact and sustained deviation, not every isolated 4xx. Trace a request across services and asynchronous work while preserving the same safe correlation identifier.

Evolve the Contract and Verify OpenAPI

Prefer compatible evolution: add optional response fields, introduce new endpoints, relax input only when it remains safe, and expand enums only when consumers handle unknown values. Changes to meaning can break a client even when JSON shape remains identical. Renaming, removing, tightening a requirement, changing nullability, altering defaults, reordering results, changing authorization visibility, or repurposing an error code all need compatibility review.

Version when a required contract must break and coexistence is necessary. NestJS supports URI, header, media-type, and custom version extraction. Choose one visible policy, define the default and unsupported-version behavior, and version the smallest coherent surface. Publish a deprecation timeline, replacement guidance, support owner, and removal condition. Observe authenticated client usage rather than assuming that an old route is unused.

Generate OpenAPI from the same controllers and runtime contract where possible, while acknowledging reflection limits. Describe authentication, authorization expectations, formats, constraints, examples, error codes, pagination, conditional headers, and idempotency. Keep response DTOs explicit so persistence columns and relations do not silently enter the specification. Review the generated document as a product artifact, not a Swagger UI decoration.

Store the specification, lint it, and compare it in CI. Fail on unapproved breaking changes, generate consumer types or mocks where useful, and run provider and consumer contract tests against a deployed build. Add endpoint tests for runtime validation, object authorization, response serialization, status and headers, duplicate requests, concurrency, and representative failures. A contract is trustworthy when the implementation, published specification, examples, and observed production behavior agree.

REST API Review Checklist

  • Semantics and ownership: resources, methods, status codes, authorization, state transitions, transaction boundaries, and side effects reflect documented domain behavior.
  • Bounded inputs: bodies, parameters, queries, headers, files, and webhooks receive runtime validation; unknown fields and ambiguous coercion follow an explicit policy.
  • Stable collections: ordering is total, pagination is bounded, cursors are opaque and valid, filters are allowlisted, and indexes match measured access patterns.
  • Safe repetition: retry-sensitive commands use durable idempotency; concurrent writes use preconditions, versions, or expected state; domain uniqueness remains enforced.
  • Verified evolution: errors and OpenAPI are stable and machine-readable; CI checks compatibility; contract tests, telemetry, deprecation, and client usage support change.
Does Your NestJS API Need a Contract Review?
Share representative endpoints, consumer types, authorization rules, error responses, retry behavior, OpenAPI, and known compatibility problems. We will identify concrete risks and shape an incremental contract-hardening plan.

We use cookies to ensure the security and proper functioning of our website. With your consent, we also use non-essential cookies for analytics and advertising purposes. You can accept or reject the use of non-essential cookies. You can change your preferences at any time. Learn more in our Cookie Policy.