Fastifyのルーティング — GET・POST・パスパラメータを定義する
この章の目次開く
ルーティングとは、どのHTTPメソッドとURLに、どの処理を対応させるかを決めることです。Fastifyでは、fastify.get()、fastify.post() などのショートハンドメソッドを使うのが基本です。
fastify.get('/todos', async () => {
return [{ id: 1, title: 'Fastifyを学ぶ', completed: false }];
});ルート定義の構文
構文: fastify.METHOD(path, options, handler)
| 引数 | 型 | 説明 |
|---|---|---|
path | string | URLのパス。:id のようにパスパラメータを定義できる |
options | object | schema、hooks、ログ設定など。省略できる |
handler | function | リクエストを処理する関数 |
戻り値: Fastifyインスタンス。通常は戻り値を意識せず、登録処理として使います。
第2引数はoptionsにもhandlerにもなる
Fastifyのショートハンドメソッドには、次の2つの書き方があります。
fastify.get(path, handler);
fastify.get(path, options, handler);| 引数の数 | 第2引数 | 第3引数 |
|---|---|---|
| 2つ | handler | なし |
| 3つ | options | handler |
optionsを使わない場合は、第2引数にhandlerを書きます。
fastify.get('/health', async () => {
return { ok: true };
});schemaなどを指定する場合は、第2引数にoptions、第3引数にhandlerを書きます。
fastify.get('/todos', {
schema: todoListSchema,
}, async () => {
return todos;
});ルートを登録するとは
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 });get()、post()、patch()などのショートハンドとfastify.route()は、いずれもルートを登録するAPIです。基本的には必要なルートを登録してからlisten()を呼びます。登録されたHTTPメソッドとURLに一致するリクエストが届いたとき、対応する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' });// routes/todos.js
export async function todoRoutes(fastify) {
fastify.get('/', async () => {
return todos;
});
fastify.get('/:id', async (request) => {
return findTodo(request.params.id);
});
}この例では、plugin内の/がGET /todos、/:idがGET /todos/:idとして登録されます。register()の構文、prefix、pluginごとの影響範囲については、pluginsとencapsulationの章で詳しく解説します。
route optionsで何を設定できるか
route optionsは、特定のルートにだけ適用する設定をまとめたオブジェクトです。入力の検証、認証などの前処理、ログ、リクエストサイズの上限といった振る舞いをhandlerの外側へ分けられます。
よく使う項目は次のとおりです。
| 項目 | 役割 | 詳細 |
|---|---|---|
schema | body、params、querystring、responseの形式を定義する | schema validationの章 |
onRequest、preHandlerなど | そのルートだけに前処理や後処理を追加する | hooksの章 |
errorHandler | そのルート専用のエラー処理を定義する | エラーハンドリングの章 |
logLevel | そのルートのログレベルを指定する | ログの章 |
bodyLimit | 受け付けるリクエストボディの最大バイト数を指定する | この章で後述 |
config | アプリケーション独自のメタデータを保存する | この章で後述 |
attachValidation | validation errorを自動送信せずrequestへ渡す | 発展的なエラー処理で使用 |
constraints | Hostや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);
});handlerには「Todoを作る」という中心処理を残し、その前後のルールをoptionsへ分けています。schemaとhookの詳しい動作は後続章で扱うため、ここでは「ルートごとの設定を第2引数にまとめられる」と捉えてください。
bodyLimitで受信サイズを制限する
bodyLimitには、そのルートで受け付けるリクエストボディの最大サイズをバイト単位で指定します。
fastify.post('/todos/import', {
bodyLimit: 64 * 1024,
}, async (request) => {
return importTodos(request.body);
});この例の上限は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,
};
});handlerやhookからはrequest.routeOptions.configで参照できます。複数ルートで共通のhookを使い、config.permissionに応じて権限を判定する、といった設計に利用できます。
fastify.route()でまとめて書く
ショートハンドを使わず、HTTPメソッド、URL、options、handlerを1つのオブジェクトにまとめる書き方もあります。
構文: fastify.route(options)
| 項目 | 型 | 説明 |
|---|---|---|
method | string / string[] | HTTPメソッド。複数指定もできる |
url | string | URLのパス。pathも別名として使える |
handler | function | リクエストを処理する関数 |
| その他 | - | schema、hooks、bodyLimit、configなどのroute options |
戻り値: Fastifyインスタンス。ショートハンドと同じく、ルートの登録処理として使います。
次の2つは、同じルートを登録します。
fastify.get('/todos', {
schema: todoListSchema,
}, listTodosHandler);fastify.route({
method: 'GET',
url: '/todos',
schema: todoListSchema,
handler: listTodosHandler,
});ショートハンドは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;
});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);
});| メソッド | 主な用途 | 例 |
|---|---|---|
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;
});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;
});このままだと completed=yes のような値も素通りします。Fastifyでは、次章以降で扱う schema を使うと、クエリやボディの形をルート単位で検証できます。
学習者じゃあ毎回 Number() や if で検証を書くんですか?
先生小さい例ではそれでも動きますが、実務では抜け漏れが出ます。Fastifyではschemaをルートに付けて、入口でまとめて検証するのが基本です。
参考リンク
次章では、handlerに渡ってくる request と reply を詳しく見て、レスポンスの返し方を整理します。