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/orders301 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
EasyYour 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.
- In
server.js, mount the tasks router at/api/v1/tasksinstead of/api/tasks. - Also in
server.js, add a middleware mounted at/api/tasksthat 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.originalUrlholds the full old address. For example:GET /api/tasks/2redirects to/api/v1/tasks/2.POST /api/tasksredirects to/api/v1/tasks.GET /api/tasks?done=false&sort=titleredirects 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.
- In
routes/tasks.js, the POST route setsLocationto the new address, for example/api/v1/tasks/4for 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
1Resources and Verbs
Resources and URLsCreating with POST4Browsers, Versions and Config
What Is an OriginAllowing Another OriginCORSA Page That Calls the APIPractice on your own: Express playground