Introduction
A well-designed API is a promise to your consumers. Change it carelessly and you break clients in production. This guide covers the design decisions that separate an API that scales gracefully from one that becomes a maintenance nightmare.
RESTful Resource Modelling
REST is not just "use HTTP verbs". It's about modelling your domain as resources and exposing them predictably.
Golden rules: •Use nouns (not verbs) for endpoints •Nest resources to show relationships — but never more than 2 levels deep •Always return consistent response envelopes
"hl-keyword">import { Router } "hl-keyword">from 'express';
"hl-keyword">const router = Router();
"hl-keyword">class="hl-comment">// ✅ Good: noun-based, consistent nesting
router.get('/users', listUsers);
router.get('/users/:id', getUser);
router.post('/users', createUser);
router.patch('/users/:id', updateUser);
router.delete('/users/:id', deleteUser);
"hl-keyword">class="hl-comment">// Nested: posts belonging to a user
router.get('/users/:id/posts', listUserPosts);
router.post('/users/:id/posts', createUserPost);
"hl-keyword">class="hl-comment">// ❌ Bad: verb "hl-keyword">in URL, inconsistent nesting
"hl-keyword">class="hl-comment">// router.post('/getUser', getUser);
"hl-keyword">class="hl-comment">// router.get('/users/:id/posts/:pid/comments/:cid/likes', ...);
"hl-keyword">export "hl-keyword">default router;
API Versioning
Never break consumers. Introduce versioning from day one.
URL versioning (most common): /v1/users, /v2/users
Header versioning: Accept: application/vnd.api.v2+json
Query param: /users?version=2 (least preferred)
"hl-keyword">class="hl-comment">// URL-based versioning — recommended "hl-keyword">for public APIs
"hl-keyword">import { Application } "hl-keyword">from 'express';
"hl-keyword">import v1Routes "hl-keyword">from './routes/v1';
"hl-keyword">import v2Routes "hl-keyword">from './routes/v2';
"hl-keyword">export "hl-keyword">function mountRoutes(app: Application) {
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
"hl-keyword">class="hl-comment">// Deprecated header warning "hl-keyword">for v1
app.use('/api/v1', (_req, res, next) => {
res.setHeader(
'Deprecation',
'true; sunset="2026-01-01"'
);
res.setHeader(
'Link',
'</api/v2>; rel="successor-version"'
);
next();
});
}
GraphQL vs REST
GraphQL lets clients request exactly the shape of data they need, eliminating over-fetching and under-fetching. The trade-off is complexity on the server.
Use REST when: •Your API is public and has many diverse consumers •Caching is critical (HTTP caching works out of the box)
Use GraphQL when: •Client requirements vary wildly (mobile vs desktop) •You're aggregating multiple microservices into one endpoint
"hl-keyword">type Query {
user(id: ID!): User
posts(filter: PostFilter, limit: Int = 20): [Post!]!
}
"hl-keyword">type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
"hl-keyword">type User {
id: ID!
name: String!
email: String!
posts: [Post!]! "hl-keyword">class="hl-comment"># resolved lazily — only fetched when requested
createdAt: DateTime!
}
"hl-keyword">type Post {
id: ID!
title: String!
body: String!
author: User!
tags: [String!]!
publishedAt: DateTime
}
input CreatePostInput {
title: String!
body: String!
tags: [String!]
}
Rate Limiting & Error Contracts
Always rate-limit public endpoints and communicate limits via standard headers. Standardise error payloads so clients can parse them programmatically — RFC 7807 (Problem Details) is the industry standard.
"hl-keyword">class="hl-comment">// RFC 7807 Problem Details
"hl-keyword">interface ProblemDetails {
"hl-keyword">type: "hl-type">string; "hl-keyword">class="hl-comment">// URI identifying error "hl-keyword">type
title: "hl-type">string; "hl-keyword">class="hl-comment">// short human-readable summary
status: "hl-type">number; "hl-keyword">class="hl-comment">// HTTP status code
detail: "hl-type">string; "hl-keyword">class="hl-comment">// human-readable explanation
instance?: "hl-type">string; "hl-keyword">class="hl-comment">// URI "hl-keyword">of the specific occurrence
}
"hl-keyword">export "hl-keyword">function notFound(resource: "hl-type">string): ProblemDetails {
"hl-keyword">return {
"hl-keyword">type: 'https:class="hl-comment">//api.thunderclap.com/errors/not-found',
title: 'Resource Not Found',
status: 404,
detail: `The requested ${resource} could not be found.`,
};
}
"hl-keyword">export "hl-keyword">function validationError(fields: Record<"hl-type">string, "hl-type">string>): ProblemDetails {
"hl-keyword">return {
"hl-keyword">type: 'https:class="hl-comment">//api.thunderclap.com/errors/validation',
title: 'Validation Failed',
status: 422,
detail: JSON.stringify(fields),
};
}