- Node.js
Node.js Express入門|Express5対応の最小APIをコピペで作る
Expressとは、Node.jsでWebサーバーやAPIを短いコードで作れる軽量なフレームワークです。 2026年10月時点の最新版は Express 5.2.1(npm の latest)で、Node.js 18 以上が必要です。4系は latest-4 タグで 4.22.3 が引き続き配布されています。
この記事では、Express 5 と ESM(import 構文)で動くタスク管理の CRUD API 一式を作り、Node.js 22.22.2 上で起動して curl の実行結果をすべて掲載します。あわせて、Express 4 から 5 への変更点を比較表にし、実際に出るエラー文を再現しました。入門記事でよく省かれる「async 関数のエラー処理」「ミドルウェアの順序」まで、コピペで確かめられる形でまとめています。
Express 5 入門の早見表
| やりたいこと | Express 5 の書き方 | ポイント |
|---|---|---|
| インストール | npm i express(5.2.1 が入る) |
Node 18 以上。4系は npm i express@4 |
| ESM で読み込む | import express from 'express' |
package.json に "type": "module" |
| アプリ作成と起動 | const app = express(); app.listen(7000) |
ポートは環境変数で切り替えると便利 |
| GET ルート | app.get('/health', (req, res) => res.json({...})) |
res.json が Content-Type も設定 |
| JSON ボディを受け取る | app.use(express.json()) → req.body |
解析されないと req.body は undefined |
| パスパラメータ | /tasks/:id → req.params.id |
値は常に文字列。数値化して検証 |
| クエリ文字列 | /tasks?done=true → req.query.done |
値は文字列。req.query は読み取り専用 |
| ステータスコード | res.status(201).json(obj) |
本文なしは res.sendStatus(204) |
| ルートをファイル分割 | const router = express.Router() → app.use('/tasks', router) |
router.get('/:id') は /tasks/:id になる |
| 404 | 全ルートの後に app.use((req, res) => ...) |
一致しなかったリクエストがここに来る |
| エラーハンドラ | app.use((err, req, res, next) => ...) |
引数4つで、最後に置く |
| async 関数のエラー | throw するだけでエラーハンドラへ |
Express 5 から自動。4 では next(err) が必要 |
| ワイルドカード | /files/*splat → req.params.splat(配列) |
5 では /* だけは書けない |
| 省略可能な部分 | /img/:file{.:ext} |
5 では ? を使わず {} で囲む |
| 静的ファイル | app.use(express.static('public')) |
ドットファイルは既定で配信しない |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Node.js Express入門|Express5対応の最小APIをコピペで作る」
Express 5 の版と動作環境を確認する
まず npm で最新版と必要な Node のバージョンを確認しておくと、記事や手順書との差に迷いません。
$ node -v
v22.22.2
$ npm view express version engines dist-tags --json
{
"version": "5.2.1",
"engines": {
"node": ">= 18"
},
"dist-tags": {
"latest": "5.2.1",
"latest-4": "4.22.3"
}
}
| 項目 | Express 5 | Express 4 |
|---|---|---|
| npm のタグ | latest(5.2.1) |
latest-4(4.22.3) |
| 必要な Node.js | 18 以上 | 0.10 以上 |
npm i express で入る版 |
こちら | npm i express@4 と明示 |
検索上位の入門記事の多くはバージョンが書かれておらず、4系の書き方が混ざっています。この記事のコードはすべて Express 5.2.1 / Node 22.22.2 で実行しています。
最小の Express アプリを作る手順
1. プロジェクトを作ってインストールする
mkdir tasks-api && cd tasks-api
npm init -y
npm pkg set type=module # ESM(import 構文)を使う設定
npm i express@5
mkdir -p src/routes public
echo '<h1>Tasks API</h1>' > public/index.html
"type": "module" を入れると .js ファイルが ESM として扱われ、import / export がそのまま使えます。 CommonJS(require)で書きたい場合はこの行を省きます。
2. 完成形のフォルダ構成
tasks-api/
├── package.json "type": "module"
├── public/
│ └── index.html 静的ファイル
└── src/
├── server.js 起動(listen)だけ
├── app.js ミドルウェアとルートの組み立て
├── store.js メモリ上のデータ保存
└── routes/
└── tasks.js /tasks 以下のルート(Router)
app.js(組み立て)と server.js(起動)を分けておくと、テストで app だけを読み込めるので後から便利です。
タスク管理 CRUD API の完成コード
src/store.js(メモリ保存)
// メモリ上のタスク保存(サーバーを止めると消えます)
const tasks = new Map();
let nextId = 1;
export function list({ done } = {}) {
const all = [...tasks.values()];
if (done === undefined) return all;
return all.filter((t) => t.done === done);
}
export function find(id) {
return tasks.get(id);
}
export function create(title) {
const task = { id: nextId++, title, done: false };
tasks.set(task.id, task);
return task;
}
export function update(id, fields) {
const task = tasks.get(id);
if (!task) return undefined;
Object.assign(task, fields);
return task;
}
export function remove(id) {
return tasks.delete(id);
}
// async の例として使う(DB 呼び出しの代わり)
export async function findAsync(id) {
await new Promise((r) => setTimeout(r, 10));
const task = tasks.get(id);
if (!task) {
const err = new Error(`task ${id} not found`);
err.status = 404;
throw err;
}
return task;
}
findAsync は、データベースへの問い合わせのような非同期処理の代わりです。見つからないときに status: 404 を付けた Error を投げ、エラーハンドラで使います。
src/routes/tasks.js(Router でルートを分割)
import { Router } from 'express';
import * as store from '../store.js';
const router = Router();
// パスパラメータ :id を数値に変換して検証(router.param でも可)
function parseId(req, res, next) {
const id = Number(req.params.id);
if (!Number.isInteger(id) || id < 1) {
return res.status(400).json({ error: 'id must be a positive integer' });
}
req.taskId = id;
next();
}
// GET /tasks?done=true 一覧(クエリで絞り込み)
router.get('/', (req, res) => {
const { done } = req.query; // 文字列 "true" / "false" / undefined
const filter = done === undefined ? {} : { done: done === 'true' };
res.json(store.list(filter));
});
// GET /tasks/:id 1件取得(async 関数。throw は自動でエラーハンドラへ)
router.get('/:id', parseId, async (req, res) => {
const task = await store.findAsync(req.taskId);
res.json(task);
});
// POST /tasks 作成
router.post('/', (req, res) => {
const title = req.body?.title;
if (typeof title !== 'string' || title.trim() === '') {
return res.status(400).json({ error: 'title is required' });
}
const task = store.create(title.trim());
res.status(201).location(`/tasks/${task.id}`).json(task);
});
// PATCH /tasks/:id 一部更新
router.patch('/:id', parseId, (req, res) => {
const { title, done } = req.body ?? {};
const fields = {};
if (title !== undefined) fields.title = String(title);
if (done !== undefined) fields.done = Boolean(done);
const task = store.update(req.taskId, fields);
if (!task) return res.status(404).json({ error: 'task not found' });
res.json(task);
});
// DELETE /tasks/:id 削除
router.delete('/:id', parseId, (req, res) => {
if (!store.remove(req.taskId)) {
return res.status(404).json({ error: 'task not found' });
}
res.sendStatus(204);
});
export default router;
ポイントは3つです。
parseIdのように、ルートの途中に関数を挟めるのがミドルウェアの基本形です。next()を呼ぶと次の関数へ進み、res.status(400).json(...)で返すとそこで終わります。req.paramsとreq.queryの値は常に文字列です。Number()や=== 'true'で型を変換します。req.body?.titleと?.を付けているのは、Express 5 では JSON として解析されなかったときreq.bodyがundefinedになるためです。
src/app.js(ミドルウェアとルートの組み立て)
import express from 'express';
import tasksRouter from './routes/tasks.js';
const app = express();
// 1. ログ(自作の最小ミドルウェア)
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
});
// 2. 静的ファイル(public/ 以下をそのまま配信)
app.use(express.static('public'));
// 3. JSON ボディの解析(Content-Type: application/json のときだけ req.body が入る)
app.use(express.json({ limit: '100kb' }));
// 4. ルーティング
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.use('/tasks', tasksRouter);
// 5. 404(どのルートにも一致しなかったとき)
app.use((req, res) => {
res.status(404).json({ error: `Not Found: ${req.method} ${req.originalUrl}` });
});
// 6. エラーハンドラ(引数が4つの関数。必ず最後に置く)
app.use((err, req, res, next) => {
const status = err.status ?? err.statusCode ?? 500;
if (status >= 500) console.error(err);
res.status(status).json({ error: status >= 500 ? 'Internal Server Error' : err.message });
});
export default app;
エラーハンドラでは、500 系のときは詳細をログにだけ出し、レスポンスには固定文言を返しています。 内部のエラー文をそのまま利用者に見せない形にしておくと安心です。エラーレスポンスの JSON 形式の決め方は「REST APIの設計」「APIエラーレスポンス設計」の記事で詳しく扱っています。
src/server.js(起動)
import app from './app.js';
const PORT = process.env.PORT ?? 7000;
app.listen(PORT, () => {
console.log(`listening on http://localhost:${PORT}`);
});
$ PORT=7000 node src/server.js
listening on http://localhost:7000
curl で全ルートを動かした実行結果
別のターミナルから curl で呼び出した結果を、そのまま載せます(Express 5.2.1 / Node 22.22.2)。
作成(POST)と一覧(GET)
$ curl -s http://localhost:7000/health
{"status":"ok"}
$ curl -s -X POST http://localhost:7000/tasks -H 'Content-Type: application/json' -d '{"title":"備品の発注"}'
{"id":1,"title":"備品の発注","done":false}
$ curl -s -X POST http://localhost:7000/tasks -H 'Content-Type: application/json' -d '{"title":"会議室の予約"}'
{"id":2,"title":"会議室の予約","done":false}
$ curl -s -i -X POST http://localhost:7000/tasks -H 'Content-Type: application/json' -d '{"title":"在庫の棚卸し"}'
HTTP/1.1 201 Created
X-Powered-By: Express
Location: /tasks/3
Content-Type: application/json; charset=utf-8
Content-Length: 50
ETag: W/"32-yFYQA1Hy7VoE5lTAZqQhxrz/IoI"
Date: Thu, 01 Oct 2026 14:33:43 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"id":3,"title":"在庫の棚卸し","done":false}
$ curl -s http://localhost:7000/tasks
[{"id":1,"title":"備品の発注","done":false},{"id":2,"title":"会議室の予約","done":false},{"id":3,"title":"在庫の棚卸し","done":false}]
res.status(201).location(...) によって、201 Created と Location ヘッダーが返っていることが確認できます。
更新(PATCH)とクエリでの絞り込み
$ curl -s -X PATCH http://localhost:7000/tasks/1 -H 'Content-Type: application/json' -d '{"done":true}'
{"id":1,"title":"備品の発注","done":true}
$ curl -s 'http://localhost:7000/tasks?done=true'
[{"id":1,"title":"備品の発注","done":true}]
$ curl -s 'http://localhost:7000/tasks?done=false'
[{"id":2,"title":"会議室の予約","done":false},{"id":3,"title":"在庫の棚卸し","done":false}]
1件取得・削除・削除後の取得
$ curl -s http://localhost:7000/tasks/2
{"id":2,"title":"会議室の予約","done":false}
$ curl -s -i -X DELETE http://localhost:7000/tasks/2
HTTP/1.1 204 No Content
X-Powered-By: Express
ETag: W/"a-bAsFyilMr4Ra1hIU5PyoyFRunpI"
Date: Thu, 01 Oct 2026 14:33:43 GMT
Connection: keep-alive
Keep-Alive: timeout=5
$ curl -s -i http://localhost:7000/tasks/2
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
(ヘッダー一部省略)
{"error":"task 2 not found"}
削除後の GET /tasks/2 は、async 関数内の throw がエラーハンドラに届いて 404 になっています。 try/catch も next(err) も書いていない点に注目してください。
入力エラーと 404
$ curl -s -i http://localhost:7000/tasks/abc
HTTP/1.1 400 Bad Request
(ヘッダー省略)
{"error":"id must be a positive integer"}
$ curl -s -X POST http://localhost:7000/tasks -H 'Content-Type: application/json' -d '{}'
{"error":"title is required"} ← 400
$ curl -s -X POST http://localhost:7000/tasks -d 'title=x'
{"error":"title is required"} ← 400(Content-Type が JSON でないので req.body は undefined)
$ curl -s -X POST http://localhost:7000/tasks -H 'Content-Type: application/json' -d '{"title":'
{"error":"Unexpected end of JSON input"} ← 400(express.json が status 400 のエラーを渡す)
$ curl -s -i http://localhost:7000/nothing
HTTP/1.1 404 Not Found
(ヘッダー省略)
{"error":"Not Found: GET /nothing"}
$ curl -s http://localhost:7000/
<h1>Tasks API</h1>
壊れた JSON を送ると、express.json() が status = 400 付きのエラーを作り、自作のエラーハンドラに渡します。 そのため 500 ではなく 400 で返せています。
サーバー側のログ
listening on http://localhost:7000
GET /health 200 19ms
POST /tasks 201 239ms
POST /tasks 201 2ms
POST /tasks 201 5ms
GET /tasks 200 1ms
PATCH /tasks/1 200 2ms
GET /tasks?done=true 200 1ms
GET /tasks?done=false 200 4ms
GET /tasks/2 200 14ms
DELETE /tasks/2 204 1ms
GET /tasks/2 404 12ms
GET /tasks/abc 400 6ms
POST /tasks 400 4ms
POST /tasks 400 1ms
POST /tasks 400 5ms
GET /nothing 404 1ms
GET / 200 25ms
res.on('finish') を使うと、レスポンスを返し終えた時点のステータスコードと処理時間を記録できます。本格的なログには morgan や pino-http などのパッケージがよく使われます。
ミドルウェアの順序を図と早見表で理解する
Express は app.use / app.get を書いた順に上から試し、next() で次へ進みます。 順序を入れ替えるだけで結果が変わるので、図で流れを押さえておきましょう。
flowchart TD
A[リクエスト] --> B[1. ログ]
B --> C{2. express.static<br>public にファイルがある?}
C -- ある --> R1[ファイルを返す]
C -- ない --> D[3. express.json<br>req.body を作る]
D --> E{4. ルート<br>/health, /tasks}
E -- 一致 --> F[ハンドラ]
E -- 不一致 --> G[5. 404 ハンドラ]
F -- 正常 --> R2[res.json で返す]
F -- throw / next err --> H[6. エラーハンドラ<br>引数4つ]
D -- JSON が壊れている --> H
H --> R3[エラーを JSON で返す]
| 順番 | 置くもの | なぜこの位置か |
|---|---|---|
| 1 | ログ | すべてのリクエストを記録するため最初に置く |
| 2 | CORS・セキュリティヘッダー | 404 やエラーのときにもヘッダーを付けたいので早めに置く |
| 3 | express.static |
ファイルがあれば即返し、以降の処理を省ける |
| 4 | express.json / express.urlencoded |
ルートより前に置かないと req.body が入らない |
| 5 | ルート(app.get、app.use('/x', router)) |
本体の処理 |
| 6 | 404 ハンドラ | どのルートにも一致しなかったときだけ届く |
| 7 | エラーハンドラ(引数4つ) | 最後。これより後のルートで起きたエラーは拾えない |
CORS は cors パッケージを app.use(cors({ origin: 'http://localhost:5173' })) のように2番目の位置に置くのが基本です。 許可するオリジンの考え方は CORS の解説記事で詳しく扱っているため、ここでは位置だけ押さえておけば十分です。
順序を入れ替えたときの実行結果
express.json() の前後にルートを置き、エラーハンドラの後ろにもルートを置いて試しました。
import express from 'express';
const app = express();
app.post('/before', (req, res) => res.json({ body: req.body ?? 'undefined' }));
app.use(express.json());
app.post('/after', (req, res) => res.json({ body: req.body }));
app.use((err, req, res, next) => res.status(500).json({ error: err.message }));
app.get('/late', (req, res) => { throw new Error('route after error handler'); });
app.listen(7004);
POST /before 200 {"body":"undefined"}
POST /after 200 {"body":{"title":"x"}}
GET /late 500 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Internal Server Error</pre> </body> </html>
| 症状 | 原因 | 直し方 |
|---|---|---|
req.body が undefined |
ルートが express.json() より前にある/Content-Type が JSON でない |
app.use(express.json()) をルートより前へ。クライアントは Content-Type: application/json を付ける |
| 自作エラーハンドラが呼ばれず HTML のエラーが返る | エラーハンドラより後ろにルートがある | エラーハンドラを一番最後に移す |
| 自作エラーハンドラが呼ばれない(引数3つ) | (err, req, res) のように引数が3つ |
必ず4つ書く。next を使わなくても省略しない |
| 404 ハンドラが全リクエストに反応する | 404 ハンドラがルートより前にある | ルートの後ろ、エラーハンドラの前へ |
なお /late の HTML は NODE_ENV=production で実行した結果です。開発時(NODE_ENV 未設定)は、Express 既定のハンドラがスタックトレースを含む HTML を返します。
Express 4 と 5 の違い(比較表と実際のエラー)
Express 5 の大きな変更は「async 関数のエラーが自動でエラーハンドラに届く」「パスの書き方が厳密になった」「古い書き方のメソッドが削除された」の3点です。 公式の移行ガイドの内容を、両方の版を入れて実行した結果とあわせて表にしました。
| 項目 | Express 4(4.22.3) | Express 5(5.2.1) |
|---|---|---|
async 関数で throw / reject |
エラーハンドラに届かない(Node 22 ではプロセスが終了) | 自動でエラーハンドラへ(500 などで返る) |
ワイルドカード /* |
書ける。値は req.params[0](文字列) |
PathError で起動時に失敗。/*splat と名前を付け、値は配列 |
省略可能 :ext? |
書ける | PathError。{.:ext} のように波かっこで囲む |
req.query への代入 |
代入できる | getter のみ。代入で TypeError |
JSON 以外の本文での req.body |
{} |
undefined |
express.urlencoded の extended 既定 |
true |
false |
express.static のドットファイル |
配信する | 既定で ignore |
app.del() |
非推奨の警告つきで動く | 削除(app.delete() を使う) |
res.json(obj, 201) |
警告つきで 201 | 第2引数は無視され 200 |
res.send(200) |
警告つきで OK を返す |
数値 200 を本文として返す |
res.redirect('back') |
使える | 削除。res.redirect(req.get('Referrer') || '/') |
req.param(name) |
使える | 削除。req.params / req.body / req.query を使う |
res.sendfile() |
使える | 削除。res.sendFile() |
| Node.js | 0.10 以上 | 18 以上 |
実行して確認した Express 5 のエラー
両方の版をインストールし、同じコードを実行しました(ルート登録時の例外を try/catch で表示)。
express 5.2.1
app.get('/*') -> PathError: Missing parameter name at index 2: /*; visit https://git.new/pathToRegexpError for info
app.get('/:file.:ext?') -> PathError: Unexpected ? at index 11: /:file.:ext?; visit https://git.new/pathToRegexpError for info
app.get('/files/*splat') OK
app.get('/img/:file{.:ext}') OK
app.del('/x') -> TypeError: app.del is not a function
GET /files/a/b/c.txt 200 {"splat":["a","b","c.txt"]}
GET /img/logo.png 200 {"file":"logo","ext":"png"}
GET /img/logo 200 {"file":"logo"}
GET /async 500 {"handled":"boom from async"}
GET /query?x=1 200 {"error":"Cannot set property query of #<IncomingMessage> which has only a getter"}
POST /body 200 {"body":{"a":1}}
POST /body (text/plain) 200 {"body":"undefined"}
GET /jsonstatus 200 {"ok":1}
GET /sendnum 200 200
同じコードを Express 4.22.3 で動かした結果です(日時とファイルパスは省略)。
express 4.22.3
app.get('/*') OK
app.get('/:file.:ext?') OK
express deprecated app.del: Use app.delete instead
app.del('/x') OK
unhandledRejection: boom from async
GET /async no response ( AbortError )
GET /query?x=1 200 {"ok":true,"query":{"a":1}}
POST /body 200 {"body":{"a":1}}
POST /body (text/plain) 200 {"body":{}}
express deprecated res.json(obj, status): Use res.status(status).json(obj) instead
GET /jsonstatus 201 {"ok":1}
express deprecated res.send(status): Use res.sendStatus(status) instead
GET /sendnum 200 OK
Express 4 では async 関数の reject がどこにも届かず、レスポンスが返らないまま待ち続けました(テストでは 1.5 秒で打ち切り)。unhandledRejection のハンドラを置かない場合は、次のようにプロセスが終了します。
$ node crash4.mjs # Express 4.22.3 + Node 22.22.2
{"0":"a/b/c.txt"}
./crash4.mjs:4
app.get('/tasks/:id', async (req, res) => { throw new Error('task not found'); });
^
Error: task not found
at ./crash4.mjs:4:51
at Layer.handle [as handle_request] (.../express/lib/router/layer.js:95:5)
...
Node.js v22.22.2
1行目の {"0":"a/b/c.txt"} は、4 の /files/* で取れるワイルドカードの値です。5 では {"splat":["a","b","c.txt"]} と名前付きの配列になるので、パスとして使うときは req.params.splat.join('/') とします。
4 から 5 へ書き換える早見表
| Express 4 の書き方 | Express 5 の書き方 |
|---|---|
app.get('*', ...) / app.get('/*', ...) |
app.get('/*splat', ...)(ルートも含めるなら '/{*splat}') |
req.params[0] |
req.params.splat.join('/') |
'/:file.:ext?' |
'/:file{.:ext}' |
promise.then(...).catch(next) |
async 関数で await するだけ |
req.query.page = 1 |
const page = Number(req.query.page ?? 1) と別の変数に |
app.del(...) |
app.delete(...) |
res.json(obj, 201) |
res.status(201).json(obj) |
res.send(204) |
res.sendStatus(204) |
res.redirect(url, 301) |
res.redirect(301, url) |
req.acceptsLanguage() など単数形 |
req.acceptsLanguages() など複数形 |
公式は自動書き換えツールも用意しています(npx codemod@latest @expressjs/v5-migration-recipe)。本記事ではこのツールは実行していません。 置き換え後は、上の比較表の項目を中心に動作を確かめると安心です。
静的ファイルとよく使う設定
express.static はフォルダをそのまま公開する組み込みミドルウェアです。 よく使うオプションをまとめます。
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// 起動場所に関係なく src/../public を指す
app.use(express.static(path.join(__dirname, '..', 'public'), {
maxAge: '1h', // Cache-Control: max-age=3600
index: 'index.html', // ディレクトリへのアクセス時に返すファイル
dotfiles: 'ignore', // Express 5 の既定。.well-known を出すなら 'allow'
}));
// URL の前に /static を付けて公開する
app.use('/static', express.static(path.join(__dirname, '..', 'assets')));
| よくある疑問 | 答え |
|---|---|
express.static('public') の基準はどこ? |
node を実行したディレクトリ。ずれるなら上のように絶対パスで指定 |
ESM で __dirname が使えない |
fileURLToPath(import.meta.url) で作る(Node 20.11 以降は import.meta.dirname も可) |
.well-known/ が 404 になる |
Express 5 は既定でドットファイルを配信しない。dotfiles: 'allow' を指定 |
X-Powered-By: Express を消したい |
app.disable('x-powered-by') |
つまずきやすいポイントのチェックリスト
| チェック | 確認方法 |
|---|---|
"type": "module" があるか |
import で SyntaxError: Cannot use import statement outside a module が出たらここ |
相対 import に拡張子 .js を付けたか |
ESM では './app.js' と拡張子まで書く |
express.json() がルートより前か |
req.body が undefined なら順序と Content-Type を確認 |
req.params.id を数値化したか |
'2' === 2 は false。Number() で変換 |
| 1つのリクエストで2回返していないか | return res.status(400)... と return を付ける |
| エラーハンドラは引数4つ・最後か | 呼ばれないときはまずこの2点 |
パスに /* や ? を使っていないか |
5 では PathError。/*splat、{} に書き換え |
return を付け忘れると、続く res.json が実行されて Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client が出ます。レスポンスを返す行には return を付ける習慣にすると防げます。
よくある質問
Q. これから学ぶなら Express 4 と 5 のどちらがよいですか?
A. Express 5 がおすすめです。 npm i express で入るのは 5 系で、async 関数のエラーを自動で扱えるため、書くコードが少なくなります。4 系の記事を読むときは、本記事の「4 から 5 へ書き換える早見表」で読み替えてください。
Q. TypeScript で書けますか?
A. 書けます。npm i -D typescript @types/express @types/node を入れ、Express 5 に対応した @types/express(5 系)を使います。本記事のコードは JavaScript(ESM)のみで実行しています。
Q. メモリ保存をデータベースに置き換えるには?
A. store.js の関数を、同じ名前の async 関数に差し替えるだけで済むように作っています。ルート側は await store.list() のように await を付ければ、Express 5 では DB のエラーもそのままエラーハンドラに届きます。
Q. ポート番号を変えたいときは?
A. PORT=3000 node src/server.js のように環境変数で渡します。server.js が process.env.PORT ?? 7000 を読む作りです。Node 20.6 以降なら node --env-file=.env src/server.js で .env ファイルからも読めます。
まとめ
- Express の最新版は 5.2.1 で、Node.js 18 以上が必要。
npm i expressで 5 系が入る - ESM なら
"type": "module"と、相対 import の拡張子.jsを忘れずに - CRUD API は
express.json・Router・404・引数4つのエラーハンドラの組み合わせで作れる - ミドルウェアは書いた順に実行される。
express.jsonはルートの前、エラーハンドラは最後 - Express 5 では async 関数の
throwが自動でエラーハンドラに届く(4 ではプロセスが終了した) - パスは
/*splatと{.:ext}、req.queryは読み取り専用、app.delなどは削除済み
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Node.js Express入門|Express5対応の最小APIをコピペで作る」
株式会社RJCでは、Node.js と Express を使った API づくりを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。