Menu
CoddyTech

バージョン管理

CoddyのNode.jsジャーニー「REST API」セクションの一部。レッスン 22/25。

回答の形式を変更すると、すでに利用しているすべてのクライアントで問題が起きます。パスにバージョンを含めれば、古いクライアントと新しいクライアントを並行して動かせます。現在は /api/v1/orders、形式を変更したら /api/v2/orders を使います。パスを移行するときは、古いパスから新しいパスへのリダイレクトを返します。

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

301 と 302 では、クライアントが POST を GET として再送することがあり、その場合は本文が失われます。308 は、メソッドと本文を含めて「同じリクエストをその場所に再送する」ことを示します。マウントされたミドルウェア内では、req.url からマウントパスが取り除かれますが、req.originalUrl にはすべて含まれています。

// GET /api/orders/5?full=1 に対する app.use('/api/orders', ...) 内では
req.url          // '/5?full=1'
req.originalUrl  // '/api/orders/5?full=1'
challenge icon

チャレンジ

簡単

API は古いクライアントが予期しない形で変更されるため、パスにバージョン番号を含めます。今日の API は /api/v1/tasks にあるバージョン 1 となり、古いアドレスからはクライアントをそこへ転送します。

  1. server.js で、tasks ルーターを /api/tasks ではなく /api/v1/tasks にマウントします。
  2. 同じく server.js で、リダイレクトのみを行うミドルウェアを /api/tasks にマウントします。ステータス 308 で、パスの残りの部分とクエリー文字列を維持したまま、同じアドレスの /api/v1 配下へ応答をリダイレクトします。マウントされたミドルウェア内では、req.originalUrl に古いアドレス全体が格納されています。例
    • GET /api/tasks/2 は /api/v1/tasks/2 へリダイレクトされます。
    • POST /api/tasks は /api/v1/tasks へリダイレクトされます。
    • GET /api/tasks?done=false&sort=title は /api/v1/tasks?done=false&sort=title へリダイレクトされます。

    301 ではなく 308 を使います。308 の後、クライアントは本文も含めて POST を POST として再送信します。

  3. routes/tasks.js で、POST ルートが Location に新しいアドレスを設定するようにします。たとえば、タスク 4 の場合は /api/v1/tasks/4 です。

試してみましょう: GET /api/tasks/2 は 308 を返し、「ヘッダー」タブには Location: /api/v1/tasks/2 が表示されます。リクエスト欄にはリダイレクト自体が表示され、リダイレクト先には移動しません。/api/tasks に { "title": "Order new chairs" } を POST すると、Location: /api/v1/tasks とともに 308 が返され、タスクは作成されません。同じ本文を /api/v1/tasks に POST すると、Location: /api/v1/tasks/4 とともに 201 が返されます。GET /api/v1/tasks/2 はタスク 2 を返します。

自分で試してみよう

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();

// BOARD_ORIGIN 上のページがブラウザからこの API を呼び出せるようにします。
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);

// ルートで発生したエラーはここで処理します。有効な JSON ではないリクエスト本文は
// クライアント側の誤り (400) で、それ以外はサーバー側の誤り (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');
});

REST APIのすべてのレッスン

自分で練習してみよう: Expressプレイグラウンド