3.9 — REST API Development: Quick Revision
Episode 3 supplement -- print-friendly.
How to use
Skim -> drill weak spots in 3.9.a through 3.9.f -> 3.9-Exercise-Questions.md.
What is REST?
- REpresentational State Transfer -- architectural style, not a protocol
- Coined by Roy Fielding (2000 PhD dissertation)
- Resources identified by URIs, manipulated via HTTP methods
- Most APIs are REST-like (Level 2) -- they skip HATEOAS, and that is fine
The 6 REST Constraints
| Constraint | Meaning |
|---|
| Client-Server | UI and data concerns separated |
| Stateless | Each request carries all info; no server-side session |
| Cacheable | Responses declare cacheability |
| Uniform Interface | Resource URIs, representations, self-descriptive messages, HATEOAS |
| Layered System | Client cannot tell if talking to origin or intermediary |
| Code on Demand | (Optional) Server can send executable code |
HTTP Methods CRUD Mapping
| Method | Operation | Idempotent | Body | Example |
|---|
GET | Read | Yes | No | GET /api/users |
POST | Create | No | Yes | POST /api/users |
PUT | Replace entirely | Yes | Yes | PUT /api/users/42 |
PATCH | Partial update | No* | Yes | PATCH /api/users/42 |
DELETE | Delete | Yes | No | DELETE /api/users/42 |
URI Best Practices
GOOD BAD
/api/users /api/getUsers (verb in URL)
/api/users/42 /api/user/42 (singular)
/api/users/42/posts /api/getUserPosts (RPC-style)
/api/users?role=admin /api/adminUsers (filter in path)
Rule: Nouns (plural) for resources, HTTP methods for actions.
Status Code Table
2xx Success
| Code | Name | When |
|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (include Location header) |
| 204 | No Content | Successful DELETE (no response body) |
4xx Client Errors
| Code | Name | When |
|---|
| 400 | Bad Request | Malformed syntax, invalid data |
| 401 | Unauthorized | Not authenticated (missing/invalid token) |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate key, state conflict |
| 422 | Unprocessable Entity | Valid JSON but fails business rules |
| 429 | Too Many Requests | Rate limit exceeded |
5xx Server Errors
| Code | Name | When |
|---|
| 500 | Internal Server Error | Unexpected server failure |
| 502 | Bad Gateway | Upstream server returned invalid response |
| 503 | Service Unavailable | Server temporarily down |
Quick mnemonic
- 401 = "Who are you?" (authentication)
- 403 = "You can't do this" (authorization)
API Versioning
| Strategy | Example | Pros | Cons |
|---|
| URL path | /api/v1/users | Explicit, cacheable, easy routing | Pollutes URLs |
| Header | Accept: vnd.api.v2+json | Clean URLs | Hard to test in browser |
| Query param | /api/users?v=2 | Simple | Hard to cache |
Most used: URL path versioning (GitHub, Stripe, Twitter).
Express setup
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Input Validation
express-validator
const { body, validationResult } = require('express-validator');
const validateUser = [
body('name').trim().notEmpty().isLength({ min: 2, max: 50 }),
body('email').isEmail().normalizeEmail(),
body('age').optional().isInt({ min: 13, max: 120 }),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
app.post('/api/users', validateUser, controller.createUser);
Zod
const { z } = require('zod');
const userSchema = z.object({
name: z.string().min(2).max(50).trim(),
email: z.string().email(),
age: z.number().int().min(13).max(120).optional()
});
Validation vs Sanitization
| Validation | Sanitization |
|---|
| Checks if data is valid | Transforms data to be safe |
isEmail(), isInt(), isLength() | trim(), escape(), normalizeEmail() |
| Accepts or rejects | Modifies the value |
Security Checklist
Helmet (Security Headers)
const helmet = require('helmet');
app.use(helmet());
Sets: Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, Referrer-Policy, and more.
CORS
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
credentials: true
}));
Rate Limiting
const rateLimit = require('express-rate-limit');
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use('/api/auth/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 10 }));
Body Size Limit
app.use(express.json({ limit: '10kb' }));
NoSQL Injection Prevention
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());
Full Security Setup
app.use(helmet());
app.use(cors({ origin: ALLOWED_ORIGINS, credentials: true }));
app.use(express.json({ limit: '10kb' }));
app.use(mongoSanitize());
app.use('/api', generalLimiter);
app.use('/api/auth', authLimiter);
Error Response Format
{
"error": {
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"details": [
{ "field": "email", "message": "Must be a valid email" }
]
}
}
Express Error Handler
app.use((err, req, res, next) => {
if (err.name === 'ValidationError') {
return res.status(400).json({ error: { message: err.message } });
}
if (err.status) {
return res.status(err.status).json({ error: { message: err.message } });
}
console.error(err);
res.status(500).json({ error: { message: 'Internal server error' } });
});
Richardson Maturity Model
Level 3: HATEOAS (links in responses) ← Truly RESTful
Level 2: HTTP verbs + resource URIs ← Most "REST" APIs
Level 1: Resources (distinct URIs)
Level 0: One endpoint, one method (RPC-style)
REST vs SOAP vs GraphQL
| REST | SOAP | GraphQL |
|---|
| Type | Style | Protocol | Query language |
| Format | JSON (any) | XML only | JSON |
| Overfetching | Common | Same | Solved (pick fields) |
| Caching | HTTP built-in | Complex | Extra tooling |
| Best for | CRUD, public APIs | Enterprise, banking | Complex data, mobile |
Postman Quick Reference
| Feature | Purpose |
|---|
| Collections | Group related requests |
| Environments | Switch between local/staging/prod variables |
| Pre-request scripts | Generate tokens, random data before request |
| Test scripts | Assert status, body, headers after response |
| Collection Runner | Execute all requests in sequence |
{{variable}} | Reference environment variables |
Common Security Headers
| Header | Purpose |
|---|
Content-Security-Policy | Controls allowed resource sources (XSS prevention) |
X-Content-Type-Options: nosniff | Prevents MIME sniffing |
X-Frame-Options: SAMEORIGIN | Prevents clickjacking |
Strict-Transport-Security | Forces HTTPS |
Referrer-Policy | Controls referrer info leakage |
X-XSS-Protection: 0 | Disables flawed browser XSS filter |
OWASP API Top 5 (Quick)
| Risk | Mitigation |
|---|
| Broken Object Auth | Check resource.owner === req.user |
| Broken Authentication | bcrypt, JWT, rate-limit login |
| Broken Property Auth | Whitelist allowed update fields |
| Unrestricted Consumption | Rate limit, body size limit, pagination |
| Security Misconfiguration | Helmet, no stack traces in prod, disable X-Powered-By |
One-Liners
- REST = stateless, resource-oriented architecture over HTTP.
- Status codes = 2xx success, 4xx client fault, 5xx server fault. Be specific.
- Versioning = URL path is most common (
/api/v1/).
- Validation = always server-side; express-validator or Zod.
- Security = helmet + CORS + rate-limit + body-limit + sanitize.
- CORS = never
origin: '*' in production with credentials.
- 401 = who are you? 403 = you can't do this.
End of 3.9 quick revision.