Skip to content
Back to Blogs
Node.jsAPIBackendExpressTypeScript

Designing APIs That Are Easier to Integrate and Maintain

Learn best practices for designing and building RESTful API boundaries using Node.js, Express, and TypeScript.

D
Dheeraj Jha
June 5, 2024
5 min read

Updated October 22, 2026

Designing APIs That Are Easier to Integrate and Maintain

An API becomes easier to work with when its behaviour is predictable. A client should know how to identify a resource, submit a valid request, handle an error, and retry safely. Those decisions matter as much as the framework used to implement the endpoint.

This guide uses Node.js, Express, and TypeScript examples. The snippets illustrate individual boundaries; database access and identity provider integration depend on the application.

Define the resource and its operations

Start with a small API contract. Consistent plural resource names such as /orders are a useful convention, but the important part is that methods and responses communicate what happened.

OperationEndpointTypical successful response
List ordersGET /orders200 with a bounded result page
Create an orderPOST /orders201 with the new resource
Read one orderGET /orders/:id200, or 404 if it is unavailable to the caller
Partially update an orderPATCH /orders/:id200 with the updated representation, or 204
Remove an orderDELETE /orders/:idOften 204 when deletion succeeds

PUT conventionally replaces a representation; PATCH expresses a partial update. Decide and document the semantics before clients depend on them. For operations such as submitting or cancelling an order, an explicit domain action may communicate intent more clearly than an unrestricted status update.

Follow the complete request path

Request handling boundaries
AuthenticateEstablish who is making the request.
Validate and authoriseCheck the input and access to the requested resource.
Apply the business operationUse transactions and database constraints where needed.
Respond and record the outcomeReturn a stable response and log a request identifier.

Keep the route handler small enough to see those steps. A service layer can hold the business operation, while a repository or database module handles persistence. Those layers are useful when they clarify ownership; they do not need to become a large class hierarchy.

Validate input before using it

TypeScript cannot validate a network payload at runtime. Treat the body, parameters, and query string as untrusted input. A schema validator such as Zod can produce a validated value for the rest of the operation.

import { z } from 'zod'

const CreateOrder = z.object({
  productId: z.string().uuid(),
  quantity: z.number().int().min(1).max(100),
})

const result = CreateOrder.safeParse(req.body)

if (!result.success) {
  res.status(400).json({
    error: {
      code: 'INVALID_ORDER',
      message: 'Check the product and quantity.',
      issues: result.error.issues,
    },
  })
  return
}

const input = result.data

Pass input to the business operation, rather than returning to the original request body. Do not accept privileged fields such as an administrator role or another tenant’s ownership ID simply because the client supplied them.

Keep authentication and authorisation separate

Authentication establishes identity. Authorisation decides what that identity may do. A valid session or JWT does not automatically grant access to every order.

For each operation, check the workspace, ownership, and role against server side policy. Apply the same scope when querying or updating the database. Browser storage and hidden UI controls are useful for presentation but cannot enforce permissions.

If using JWTs, validate signatures, expected algorithms, issuer, audience, and expiry according to the identity provider’s contract. If using cookies, choose appropriate cookie attributes and CSRF protection for the request model. Avoid copying a short token example and treating it as a complete identity system.

Return useful, consistent errors

Use stable error codes that clients can handle, a readable message, and a request ID where available. Keep sensitive database details and stack traces in protected server logs.

{
  "error": {
    "code": "ORDER_ALREADY_SUBMITTED",
    "message": "This order can no longer be edited."
  }
}

Separate expected errors, such as invalid input or a business conflict, from unexpected failures. An unavailable database is not a client validation error and should not become a generic 400 response.

Express error middleware runs after route handlers and has four arguments. Express 5 forwards rejected promises returned by handlers to the error chain; older Express versions require explicit forwarding or an async wrapper. Check the version before choosing a pattern.

Bound list queries

A list endpoint should have a validated page size and a deterministic sort order. Reject invalid parameters, cap the maximum limit, and include a unique tie breaker in the ordering.

For large or changing datasets, cursor pagination often gives a more stable traversal than a large offset. A typical order cursor might include both created_at and id. Scope queries to the authorised workspace and parameterise all values rather than concatenating user input into SQL.

Make retries and slow operations explicit

Clients retry when a response is lost. For operations that create an order or trigger a payment, define an idempotency key and persist its association with the outcome. This prevents a repeated request from creating a second side effect.

For document generation or ingestion, return a durable job identifier and let the client check status. Sending the job to a queue should have a recovery strategy if enqueueing fails after the database commit, such as a transactional outbox.

Before publishing the API

  • Document request schemas, response schemas, permissions, and error codes in OpenAPI or an equivalent contract.
  • Test success, invalid input, unauthorised access, cross tenant access, conflicts, and retry behaviour.
  • Use rate limits appropriate to the route, with shared accounting when multiple instances need a common limit.
  • Configure proxy trust and CORS for the actual deployment. CORS controls browser access to responses; it is not authentication.
  • Capture latency, error rate, and request IDs so failures can be investigated.

A useful API has clear rules for both successful and unsuccessful requests. When those rules are visible in the contract and enforced at the boundary, clients need less guesswork and backend changes become easier to review.

References