Intermediate
Quick reference for backend REST APIs — HTTP methods, status codes, Express middleware patterns, validation, pagination, auth headers, CORS, rate limiting, and REST vs GraphQL.
RESTHTTPExpressAPI Design
HTTP Methods
| Method |
Use |
Safe |
Idempotent |
| GET | Read | ✓ | ✓ |
| POST | Create | ✗ | ✗ |
| PUT | Replace | ✗ | ✓ |
| PATCH | Partial update | ✗ | ~ |
| DELETE | Remove | ✗ | ✓ |
Status Codes
| Code | Meaning |
| 200 | OK — GET/PUT/PATCH success |
| 201 | Created — POST success |
| 204 | No Content — DELETE success |
| 400 | Bad Request — malformed |
| 401 | Unauthorized — no/invalid auth |
| 403 | Forbidden — no permission |
| 404 | Not Found |
| 409 | Conflict — duplicate/version |
| 422 | Unprocessable — invalid data |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
| 503 | Service Unavailable |
REST URL Design
GET /v1/users # list
POST /v1/users # create
GET /v1/users/42 # read one
PUT /v1/users/42 # full replace
PATCH /v1/users/42 # partial update
DELETE /v1/users/42 # delete
GET /v1/users/42/orders # nested resource
# plural nouns, no verbs. version in path.
# GET /getUser ✗ GET /v1/users/42 ✓
Express Essentials
Server + Route
import express from 'express';
const app = express();
app.use(express.json());
app.get('/v1/users/:id', async (req, res, next) => {
try {
const u = await db.users.find(req.params.id);
if (!u) return res.status(404).json({ error: 'not found' });
res.json(u);
} catch (e) { next(e); }
});
app.post('/v1/users', async (req, res, next) => {
try {
const c = await db.users.create(req.body);
res.status(201).json(c);
} catch (e) { next(e); }
});
app.listen(3000);
Middleware & Error Handler
// middleware: (req, res, next)
app.use((req, res, next) => { console.log(req.path); next(); });
// error handler: 4 args, registered LAST
app.use((err, req, res, next) => {
const status = err.status ?? 500;
if (status >= 500) console.error(err);
res.status(status).json({ error: err.publicMessage ?? 'error' });
});
Validation (Zod)
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
age: z.number().int().min(0),
});
const r = schema.safeParse(req.body);
if (!r.success) return res.status(422).json({ errors: r.error.issues });
req.body = r.data; // typed, sanitized
Pagination & Filtering
# query params
GET /v1/orders?status=paid&sort=-createdAt&limit=20&cursor=abc
const limit = Math.min(Number(req.query.limit) || 20, 100);
res.json({ data: items, nextCursor }); // cursor > offset for big data
Auth, CORS, Rate Limit
Bearer Auth
// Authorization: Bearer <token>
const token = (req.get('authorization') || '').replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'missing token' });
req.user = verifyJwt(token); // 401 if invalid
// authorize: 403 if authenticated but not allowed
if (!req.user.roles.includes('admin')) return res.sendStatus(403);
CORS
import cors from 'cors';
app.use(cors({
origin: ['https://app.example.com'], // allow-list, not '*'
credentials: true,
})); // browser preflights non-simple reqs with OPTIONS
Rate Limit
import rateLimit from 'express-rate-limit';
app.use(rateLimit({ windowMs: 60_000, limit: 100 }));
// respond 429 + Retry-After; back with Redis across instances
REST vs GraphQL
| Aspect | REST | GraphQL |
| Endpoints | Many | One |
| Fetch shape | Fixed | Client-chosen |
| Caching | Easy (HTTP) | Harder |
| Over-fetch | Common | Avoided |
| Best for | Public/cacheable | Rich clients |
Design Checklist
- Plural noun resources, no verbs in URLs
- Correct method + precise status code
- Validate all input at the edge → 400/422
- Paginate every list (cursor preferred)
- Version the base path (/v1)
- Authenticate (401) then authorize (403)
- Allow-list CORS origins; rate-limit → 429
- Publish an OpenAPI 3.1 spec as the contract