本文へスキップ
ウェブエンジニア問題集
第3章

Fastifyのルーティング — GET・POST・パスパラメータを定義する

約10分
この章の目次開く

ルーティングとは、どのHTTPメソッドとURLに、どの処理を対応させるかを決めることです。Fastifyでは、fastify.get()、fastify.post() などのショートハンドメソッドを使うのが基本です。

fastify.get('/todos', async () => {
  return [{ id: 1, title: 'Fastifyを学ぶ', completed: false }];
});
js

ルート定義の構文

構文: fastify.METHOD(path, options, handler)

引数型説明
pathstringURLのパス。:id のようにパスパラメータを定義できる
optionsobjectschema、hooks、ログ設定など。省略できる
handlerfunctionリクエストを処理する関数

戻り値: Fastifyインスタンス。通常は戻り値を意識せず、登録処理として使います。

第2引数はoptionsにもhandlerにもなる

Fastifyのショートハンドメソッドには、次の2つの書き方があります。

fastify.get(path, handler);
fastify.get(path, options, handler);
js
引数の数第2引数第3引数
2つhandlerなし
3つoptionshandler

optionsを使わない場合は、第2引数にhandlerを書きます。

fastify.get('/health', async () => {
  return { ok: true };
});
js

schemaなどを指定する場合は、第2引数にoptions、第3引数にhandlerを書きます。

fastify.get('/todos', {
  schema: todoListSchema,
}, async () => {
  return todos;
});
js
引数が2つなら第2引数はhandler、3つなら第2引数はそのルート専用のoptionsです。

ルートを登録するとは

fastify.get()やfastify.post()を呼ぶと、その場でhandlerが実行されるわけではありません。「このHTTPメソッドとURLにリクエストが届いたら、このhandlerを実行する」という対応関係がFastifyへ登録されます。

const fastify = Fastify({ logger: true });
 
// GET /todosとhandlerの対応関係を登録する
fastify.get('/todos', async () => {
  return todos;
});
 
// 登録を終えてからリクエストの受け付けを開始する
await fastify.listen({ port: 3000 });
js

get()、post()、patch()などのショートハンドとfastify.route()は、いずれもルートを登録するAPIです。基本的には必要なルートを登録してからlisten()を呼びます。登録されたHTTPメソッドとURLに一致するリクエストが届いたとき、対応するhandlerが実行されます。

ルート登録は処理の予約、handler実行はリクエストが届いた後です。

ルートが増えてきたら、Todo関連、ユーザー関連、管理画面関連のようにルート群をplugin関数へ分け、fastify.register()で登録します。

import { todoRoutes } from './routes/todos.js';
import { userRoutes } from './routes/users.js';
 
fastify.register(todoRoutes, { prefix: '/todos' });
fastify.register(userRoutes, { prefix: '/users' });
js
// routes/todos.js
export async function todoRoutes(fastify) {
  fastify.get('/', async () => {
    return todos;
  });
 
  fastify.get('/:id', async (request) => {
    return findTodo(request.params.id);
  });
}
js

この例では、plugin内の/がGET /todos、/:idがGET /todos/:idとして登録されます。register()の構文、prefix、pluginごとの影響範囲については、pluginsとencapsulationの章で詳しく解説します。

route optionsで何を設定できるか

route optionsは、特定のルートにだけ適用する設定をまとめたオブジェクトです。入力の検証、認証などの前処理、ログ、リクエストサイズの上限といった振る舞いをhandlerの外側へ分けられます。

よく使う項目は次のとおりです。

項目役割詳細
schemabody、params、querystring、responseの形式を定義するschema validationの章
onRequest、preHandlerなどそのルートだけに前処理や後処理を追加するhooksの章
errorHandlerそのルート専用のエラー処理を定義するエラーハンドリングの章
logLevelそのルートのログレベルを指定するログの章
bodyLimit受け付けるリクエストボディの最大バイト数を指定するこの章で後述
configアプリケーション独自のメタデータを保存するこの章で後述
attachValidationvalidation errorを自動送信せずrequestへ渡す発展的なエラー処理で使用
constraintsHostやAPIバージョンなど、ルートが一致する条件を追加する発展的なルーティングで使用

Fastifyにはほかにもroute optionsがありますが、最初からすべて覚える必要はありません。まずはschemaとpreHandlerを中心に、必要になった設定を追加していけば十分です。

複数のoptionsを組み合わせる

route optionsの各項目は、1つのオブジェクト内で組み合わせられます。Todo作成APIに入力検証、認証、ボディサイズの上限、独自メタデータを付ける例を見てみましょう。

const createTodoOptions = {
  schema: {
    body: {
      type: 'object',
      required: ['title'],
      properties: {
        title: { type: 'string', minLength: 1 },
      },
    },
  },
  preHandler: async (request, reply) => {
    if (!request.headers.authorization) {
      return reply.code(401).send({ error: 'UNAUTHORIZED' });
    }
  },
  bodyLimit: 16 * 1024,
  config: {
    permission: 'todo:create',
  },
};
 
fastify.post('/todos', createTodoOptions, async (request, reply) => {
  const todo = createTodo(request.body.title);
  return reply.code(201).send(todo);
});
js

handlerには「Todoを作る」という中心処理を残し、その前後のルールをoptionsへ分けています。schemaとhookの詳しい動作は後続章で扱うため、ここでは「ルートごとの設定を第2引数にまとめられる」と捉えてください。

bodyLimitで受信サイズを制限する

bodyLimitには、そのルートで受け付けるリクエストボディの最大サイズをバイト単位で指定します。

fastify.post('/todos/import', {
  bodyLimit: 64 * 1024,
}, async (request) => {
  return importTodos(request.body);
});
js

この例の上限は64KiBです。上限を超えたリクエストはhandlerへ到達する前に拒否されます。アプリ全体にも上限を設定できますが、route optionsへ書くと、インポートAPIなど特定のルートだけ別の値にできます。

configに独自情報を持たせる

configはFastifyが用途を決めていない自由なオブジェクトです。権限名や監査対象かどうかなど、アプリケーション独自の情報をルートへ付けられます。

fastify.get('/reports', {
  config: {
    permission: 'report:read',
    audit: true,
  },
}, async (request) => {
  const routeConfig = request.routeOptions.config;
 
  return {
    requiredPermission: routeConfig.permission,
  };
});
js

handlerやhookからはrequest.routeOptions.configで参照できます。複数ルートで共通のhookを使い、config.permissionに応じて権限を判定する、といった設計に利用できます。

fastify.route()でまとめて書く

ショートハンドを使わず、HTTPメソッド、URL、options、handlerを1つのオブジェクトにまとめる書き方もあります。

構文: fastify.route(options)

項目型説明
methodstring / string[]HTTPメソッド。複数指定もできる
urlstringURLのパス。pathも別名として使える
handlerfunctionリクエストを処理する関数
その他-schema、hooks、bodyLimit、configなどのroute options

戻り値: Fastifyインスタンス。ショートハンドと同じく、ルートの登録処理として使います。

次の2つは、同じルートを登録します。

fastify.get('/todos', {
  schema: todoListSchema,
}, listTodosHandler);
js
fastify.route({
  method: 'GET',
  url: '/todos',
  schema: todoListSchema,
  handler: listTodosHandler,
});
js

ショートハンドはHTTPメソッドが見やすく、fastify.route()はルート定義を1つのオブジェクトとして管理しやすいという違いがあります。プロジェクト内で読みやすい方へ揃えれば構いません。

schemaを指定する例

response schemaをoptionsへ指定すると、Fastifyがレスポンスの形式に合わせてシリアライズします。ここではroute optionsとしてどこへ書くかを確認します。

fastify.get('/todos', {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: { type: 'number' },
            title: { type: 'string' },
            completed: { type: 'boolean' },
          },
        },
      },
    },
  },
}, async () => {
  return todos;
});
js

schemaによる入力検証、型変換、レスポンスのシリアライズは、schema validationの章で詳しく解説します。

HTTPメソッドごとに処理を分ける

Todo APIの入り口を作ってみます。

const todos = [
  { id: 1, title: 'Fastifyを学ぶ', completed: false },
];
 
fastify.get('/todos', async () => {
  return todos;
});
 
fastify.post('/todos', async (request, reply) => {
  const todo = {
    id: todos.length + 1,
    title: request.body.title,
    completed: false,
  };
 
  todos.push(todo);
 
  return reply.code(201).send(todo);
});
js
メソッド主な用途例
GET取得Todo一覧を返す
POST作成Todoを追加する
PUT全体更新Todo全体を置き換える
PATCH部分更新completed だけ変更する
DELETE削除Todoを削除する

パスパラメータ

URLの一部を変数として受け取るには、:id のように書きます。

fastify.get('/todos/:id', async (request, reply) => {
  const id = Number(request.params.id);
  const todo = todos.find((item) => item.id === id);
 
  if (!todo) {
    return reply.code(404).send({ error: 'TODO_NOT_FOUND' });
  }
 
  return todo;
});
js

GET /todos/1 にアクセスすると、request.params.id は '1' になります。

request.params や request.query の値は基本的に文字列として入ってくるため、数値として使う前に変換と検証が必要です。

クエリ文字列

GET /todos?completed=true のような値は、request.query から取り出します。

fastify.get('/todos', async (request) => {
  const completed = request.query.completed;
 
  if (completed === 'true') {
    return todos.filter((todo) => todo.completed);
  }
 
  if (completed === 'false') {
    return todos.filter((todo) => !todo.completed);
  }
 
  return todos;
});
js

このままだと completed=yes のような値も素通りします。Fastifyでは、次章以降で扱う schema を使うと、クエリやボディの形をルート単位で検証できます。

学習者学習者

じゃあ毎回 Number() や if で検証を書くんですか?

先生先生

小さい例ではそれでも動きますが、実務では抜け漏れが出ます。Fastifyではschemaをルートに付けて、入口でまとめて検証するのが基本です。

参考リンク

次章では、handlerに渡ってくる request と reply を詳しく見て、レスポンスの返し方を整理します。

Node.jsクイズに挑戦するHTTPメソッドやルーティングの基本をクイズで確認しよう