Episode 3 — NodeJS MongoDB Backend Architecture / 3.9 — REST API Development

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

ConstraintMeaning
Client-ServerUI and data concerns separated
StatelessEach request carries all info; no server-side session
CacheableResponses declare cacheability
Uniform InterfaceResource URIs, representations, self-descriptive messages, HATEOAS
Layered SystemClient cannot tell if talking to origin or intermediary
Code on Demand(Optional) Server can send executable code

HTTP Methods CRUD Mapping

MethodOperationIdempotentBodyExample
GETReadYesNoGET /api/users
POSTCreateNoYesPOST /api/users
PUTReplace entirelyYesYesPUT /api/users/42
PATCHPartial updateNo*YesPATCH /api/users/42
DELETEDeleteYesNoDELETE /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

CodeNameWhen
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST (include Location header)
204No ContentSuccessful DELETE (no response body)

4xx Client Errors

CodeNameWhen
400Bad RequestMalformed syntax, invalid data
401UnauthorizedNot authenticated (missing/invalid token)
403ForbiddenAuthenticated but not authorized
404Not FoundResource does not exist
409ConflictDuplicate key, state conflict
422Unprocessable EntityValid JSON but fails business rules
429Too Many RequestsRate limit exceeded

5xx Server Errors

CodeNameWhen
500Internal Server ErrorUnexpected server failure
502Bad GatewayUpstream server returned invalid response
503Service UnavailableServer temporarily down

Quick mnemonic

  • 401 = "Who are you?" (authentication)
  • 403 = "You can't do this" (authorization)

API Versioning

StrategyExampleProsCons
URL path/api/v1/usersExplicit, cacheable, easy routingPollutes URLs
HeaderAccept: vnd.api.v2+jsonClean URLsHard to test in browser
Query param/api/users?v=2SimpleHard 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()
});

// In route: const data = userSchema.parse(req.body);
// Throws ZodError if invalid

Validation vs Sanitization

ValidationSanitization
Checks if data is validTransforms data to be safe
isEmail(), isInt(), isLength()trim(), escape(), normalizeEmail()
Accepts or rejectsModifies 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'],       // whitelist, NOT '*'
  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());
// Strips $ and . from req.body, req.query, req.params

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

RESTSOAPGraphQL
TypeStyleProtocolQuery language
FormatJSON (any)XML onlyJSON
OverfetchingCommonSameSolved (pick fields)
CachingHTTP built-inComplexExtra tooling
Best forCRUD, public APIsEnterprise, bankingComplex data, mobile

Postman Quick Reference

FeaturePurpose
CollectionsGroup related requests
EnvironmentsSwitch between local/staging/prod variables
Pre-request scriptsGenerate tokens, random data before request
Test scriptsAssert status, body, headers after response
Collection RunnerExecute all requests in sequence
{{variable}}Reference environment variables

Common Security Headers

HeaderPurpose
Content-Security-PolicyControls allowed resource sources (XSS prevention)
X-Content-Type-Options: nosniffPrevents MIME sniffing
X-Frame-Options: SAMEORIGINPrevents clickjacking
Strict-Transport-SecurityForces HTTPS
Referrer-PolicyControls referrer info leakage
X-XSS-Protection: 0Disables flawed browser XSS filter

OWASP API Top 5 (Quick)

RiskMitigation
Broken Object AuthCheck resource.owner === req.user
Broken Authenticationbcrypt, JWT, rate-limit login
Broken Property AuthWhitelist allowed update fields
Unrestricted ConsumptionRate limit, body size limit, pagination
Security MisconfigurationHelmet, 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.