Backend and API design patterns — RESTful and GraphQL API conventions, error handling, pagination, authentication patterns, and database best practices.
Backend and API design patterns — RESTful and GraphQL API conventions, error handling, pagination, authentication patterns, and database best practices.
Apply these backend practices when building services and APIs:
Resource naming — use nouns, not verbs:
GET /users → list users
POST /users → create a user
GET /users/:id → get a user
PATCH /users/:id → partially update a user
PUT /users/:id → replace a user
DELETE /users/:id → delete a user
GET /users/:id/posts → list a user's posts
HTTP status codes:
| Situation | Code |
|---|---|
| OK / resource returned | 200 |
| Created | 201 |
| No content (DELETE) | 204 |
| Bad request (validation) |
| 400 |
| Unauthenticated | 401 |
| Forbidden | 403 |
| Not found | 404 |
| Conflict (duplicate) | 409 |
| Unprocessable entity | 422 |
| Server error | 500 |
Versioning: prefix routes with /v1/, /v2/ etc. Never break a published API version.
Return consistent, machine-readable errors:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Must be a valid email address" }
],
"traceId": "abc123"
}
}
Always include a traceId for debuggability in production.
Use cursor-based pagination for large or real-time datasets; offset for simple cases:
Cursor-based (preferred):
{
"data": [...],
"pagination": {
"hasNextPage": true,
"endCursor": "eyJpZCI6MTAwfQ=="
}
}
Offset-based:
{
"data": [...],
"pagination": {
"page": 2,
"perPage": 20,
"total": 543
}
}
HttpOnly cookie)HttpOnly; Secure; SameSite=Strict cookieValidate tokens on every request. Never trust client-supplied user IDs without re-verifying the token.
Schema design:
created_at and updated_at timestamps to every tabledeleted_at) for data that needs audit trailsQuery discipline:
.include(), JOIN, dataloader)Migrations:
Organise code in layers:
Controller (HTTP) → Service (business logic) → Repository (data access)
// Controller: validate input, delegate to service
app.post('/users', async (req, res) => {
const parsed = createUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(422).json({ error: parsed.error.format() });
}
const user = await userService.create(parsed.data);
res.status(201).json(user);
});
// Service: business logic only
class UserService {
async create(data: CreateUserDto) {
const existing = await this.userRepo.findByEmail(data.email);
if (existing) throw new ConflictError('Email already registered');
return this.userRepo.create({ ...data, passwordHash: await hash(data.password) });
}
}