Menu
CoddyTech

Versioning

Part of the REST APIs section of Coddy's Node.js journey. Lesson 22 of 25.

Changing the shape of an answer breaks every client already using it. A version in the path lets old and new clients live side by side: /api/v1/orders today, /api/v2/orders when the shape changes. When a path moves, the old path responds with a redirect to the new one:

GET  /api/orders/5   →  308  Location: /api/v1/orders/5
POST /api/orders     →  308  Location: /api/v1/orders

301 and 302 let clients repeat a POST as a GET, which drops the body. 308 says "repeat the same request there", method and body included. Inside a mounted middleware, req.url has lost the mount path, while req.originalUrl keeps all of it:

// in app.use('/api/orders', ...) for GET /api/orders/5?full=1
req.url          // '/5?full=1'
req.originalUrl  // '/api/orders/5?full=1'
challenge icon

Challenge

Easy

Your API will change in ways old clients won't expect, so put a version number in its paths. Today's API becomes version 1, at /api/v1/tasks, and the old addresses send clients there.

  1. In server.js, mount the tasks router at /api/v1/tasks instead of /api/tasks.
  2. Also in server.js, add a middleware mounted at /api/tasks that only redirects. It responds with status 308 to the same address under /api/v1, keeping the rest of the path and the query string. Inside a mounted middleware, req.originalUrl holds the full old address. For example:
    • GET /api/tasks/2 redirects to /api/v1/tasks/2.
    • POST /api/tasks redirects to /api/v1/tasks.
    • GET /api/tasks?done=false&sort=title redirects to /api/v1/tasks?done=false&sort=title.

    Use 308, not 301: after a 308, a client repeats a POST as a POST, body included.

  3. In routes/tasks.js, the POST route sets Location to the new address, for example /api/v1/tasks/4 for task 4.

Try it: GET /api/tasks/2 returns 308, and the Headers tab shows Location: /api/v1/tasks/2. The request bar shows the redirect itself; it does not follow it. POST { "title": "Order new chairs" } to /api/tasks: you get 308 with Location: /api/v1/tasks, and no task is created. POST the same body to /api/v1/tasks: 201 with Location: /api/v1/tasks/4. GET /api/v1/tasks/2 returns task 2.

Try it yourself

import express from 'express';
import { sendError } from './errors.js';
import taskRoutes from './routes/tasks.js';

const BOARD_ORIGIN = 'https://board.example.com';

const app = express();

// Lets pages on BOARD_ORIGIN call this API from the browser.
app.use((req, res, next) => {
    res.set('Access-Control-Allow-Origin', BOARD_ORIGIN);

    if (req.method === 'OPTIONS') {
        res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
        res.set('Access-Control-Allow-Headers', 'Content-Type');
        return res.status(204).end();
    }
    next();
});

app.use(express.json());

app.use(express.static('public'));

app.use('/api/tasks', taskRoutes);

// Errors from the routes end here. A body that is not valid JSON is the
// client's mistake (400); anything else is ours (500).
app.use((err, req, res, next) => {
    if (err.type === 'entity.parse.failed') {
        return sendError(res, 400, 'invalid_json', 'The body is not valid JSON');
    }
    console.error(err);
    sendError(res, 500, 'server_error', 'Something went wrong');
});

app.listen(3000, () => {
    console.log('Task tracker API on port 3000');
});

All lessons in REST APIs

Practice on your own: Express playground