🥑

Express入門~Node.jsのための最小限のWebフレームワーク~

2024/09/01に公開

業務で使う前にある程度把握したい方やはじめてプログラミングを学びたい方向けに簡単にまとめてみました。

全部読む必要がなくて、部分的に読みたい方もいらっしゃると思いますので、サンプルは全て単話形式で載せています。
記事の途中から読み始めるでも全然OKかと思いますのでお気軽に読んでください!

概要は不要で、単に実際のコードを試したいだけの方は、記事の最後におすすめハンズオンも載せているのでこちらだけでも試すと良いかもしれません。

1.Expressとは

Expressは、Node.js上で動作するシンプルで柔軟なWebフレームワークです。
Webアプリケーションの機能(ルーティング、データベース接続、セッション管理など)が組み込まれているため、コードの再利用がしやすくなり、効率的に高品質なWebアプリケーションを作成できます

以下の4つの記述方法について把握すれば入門は終わりといっても過言ではないと思います。

1.ルーティング

ざっくり「URLとそれに対する応答(レスポンス)を結びつける役割」。例えば、ユーザーが「/home」というURLにアクセスしたときに、ホームページを表示するなど

2.ミドルウェア

リクエストがサーバーに到達してから、最終的なレスポンスがクライアントに返されるまでの間に、リクエストやレスポンスに対して何らかの処理を行うための関数

3.テンプレートエンジン

あらかじめ作成したテンプレートファイルに変数を入れて、動的なHTMLを簡単に作成できる。実行時に実際のデータに置き換えられ、最終的なHTMLファイルとしてクライアントに送信する。

4.エラーハンドリングとデバッグ

Expressはデフォルトで同期非同期的なエラーハンドラが用意されています

2.Expressのインストールと初期設定

https://expressjs.com/ja/starter/installing.html
上記を参考にした以下のコマンドを実行してください。

mkdir myapp
cd myapp
npm init
# 上記コマンド実行後、いくつか入力事項を聞かれる。基本全てenterでOK
npm install express

3.Expressの主な機能

3-1.ルーティングの詳細

Expressでは、ルートとメソッドを指定することで、一致するリクエストをリッスンしコールバック関数を呼びます。

const express = require('express')
const app = express() //Expressのappオブジェクトのメソッドを使用して定義

// GETリクエストが来たら"hello world"を返す
app.get('/', (req, res) => { //①ルート(例:: '/')とメソッド(例: get)が一致
  res.send('hello world') //②上記①の一致が検知されるとコールバック関数が呼ばれる
})

nextを使うと制御を次の関数に渡すことができる。
以下のパターンで使うことが多いです。

  1. 基本的なミドルウェアのチェーン
app.use((req, res, next) => {
    console.log('Request received');
    next();  // 次のミドルウェアへ進む
});

// 次の処理
app.use((req, res, next) => {
    console.log('Handling request');
    res.send('Hello World!');
});
  1. 条件付きで次のミドルウェアを呼び出す
app.use((req, res, next) => {
    if (req.query.token === '12345') {
        next();  // トークンが正しい場合、次のミドルウェアへ進む
    } else {
        res.status(403).send('Forbidden'); // 不正な場合は次に進まず、403を返す
    }
});
  1. エラーハンドリングのためのnextの使用
app.use((req, res, next) => {
    try {
        // 何かしらの処理
        throw new Error('Something went wrong!');
    } catch (err) {
        next(err);  // エラーを次のエラーハンドリングミドルウェアに渡す
    }
});

// 次の処理
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});
  1. 特定のルートに対するミドルウェアの連鎖
app.get('/user/:id', (req, res, next) => {
    console.log(`Request Type: ${req.method}`);
    next();  // 次のハンドラへ進む
}, (req, res, next) => {
    res.send(`User ID: ${req.params.id}`);
});

ルートパラメータとクエリパラメータ

ルートパラメータ

app.get("/:group_id/:user_id", (req, res) => {
  res.send(req.params); // => {"group_id": "123", "user_id": "ABC"}
});

クエリ

// http://www.example.com/user?group_id=123&user_id=ABC
app.get("/user", (req, res) => {
  res.send(req.query); // => {"group_id": "123", "user_id": "ABC"}
});

RESTfulルーティング

GET /users: 全ユーザーのリストを取得
GET /users/:id: 特定のユーザーを取得
POST /users: 新しいユーザーを作成
PUT /users/:id: 特定のユーザーの情報を更新
DELETE /users/:id: 特定のユーザーを削除

const express = require('express');
const app = express();

// ボディパーサーが必要な場合
app.use(express.json());

// 仮のデータベース
let users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

// 1. 全ユーザーのリストを取得 (GET /users)
app.get('/users', (req, res) => {
  res.json(users);
});

// 2. 特定のユーザーを取得 (GET /users/:id)
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).send('User not found');
  res.json(user);
});

// 3. 新しいユーザーを作成 (POST /users)
app.post('/users', (req, res) => {
  const newUser = {
    id: users.length + 1,
    name: req.body.name
  };
  users.push(newUser);
  res.status(201).json(newUser);
});

// 4. 特定のユーザーの情報を更新 (PUT /users/:id)
app.put('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).send('User not found');

  user.name = req.body.name;
  res.json(user);
});

// 5. 特定のユーザーを削除 (DELETE /users/:id)
app.delete('/users/:id', (req, res) => {
  const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
  if (userIndex === -1) return res.status(404).send('User not found');

  users.splice(userIndex, 1);
  res.status(204).send(); // 204 No Content
});

// サーバーの起動
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

ネストされたルーティング

const express = require('express');
const app = express();
const router = express.Router();

// メインルート (例: /users)
router.get('/', (req, res) => {
  res.send('ユーザー一覧');
});

// サブルート (例: /users/:id)
router.get('/:id', (req, res) => {
  res.send(`ユーザーID: ${req.params.id}`);
});

// さらにネストされたサブルート (例: /users/:id/profile)
router.get('/:id/profile', (req, res) => {
  res.send(`ユーザーID: ${req.params.id}のプロフィール`);
});

// ルートをアプリケーションに登録
app.use('/users', router);

// サーバーの起動
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

3-2ミドルウェアの理解

ミドルウェア関数とは

ミドルウェア関数とはリクエストとレスポンスの処理をカスタマイズする関数のことです。
具体的には以下を行うことが多いです。

  • 任意のコードを実行する。
  • リクエストオブジェクトとレスポンスオブジェクトを変更する。
  • スタック内の次のミドルウェアを呼び出す。
  • リクエストレスポンスサイクルを終了する。

Expressのミドルウェア関数の種類

Expressには主に以下の4つのような種類のミドルウェアがあります。

  1. アプリケーション・レベルのミドルウェア:
    app.use()やapp.METHOD()(例:app.get())を使って、アプリケーション全体で動作するミドルウェアです。すべてのルートや特定のHTTPメソッドに対して共通の処理を行います。

  2. ルーター・レベルのミドルウェア:
    express.Router()を使って、特定のルートや一部のルートグループでのみ動作するミドルウェアです。app.use('/api', router)のように、サブアプリケーションのような形で組織化できます。

  3. エラーハンドリング・ミドルウェア:
    特別なタイプのミドルウェアで、4つの引数(err, req, res, next)を受け取り、エラー発生時にのみ実行されます。エラーハンドリングやエラーメッセージのカスタマイズに使います。

  4. 標準装備・サードパーティーのミドルウェア:
    HTMLファイルや画像などの静的リソースの提供やJSONペイロードで受信したリクエストの解析などはExpressに標準で実装されています。また他に必要なものがあればサードパーティのミドルウェアを利用することで実装を効率的に進められます。

カスタムミドルウェアの作成

Expressではapp.useを
Expressでよくあるカスタムミドルウェアの簡単な実装例をいくつかみてみましょう。

  1. リクエストの処理: クライアントからのリクエストを受け取り、その内容をチェックしたり、変更したりします。たとえば、リクエストの認証やログ記録を行うことができます。
  2. レスポンスの設定: サーバーがクライアントに返すレスポンスを準備します。例えば、レスポンスのヘッダーを設定したり、レスポンスのデータを加工したりできます。
  3. リクエストの次の処理へ進む: 一連のミドルウェア関数が順番に実行され、処理が次のミドルウェア関数に渡されます。これにより、複数の処理を順に実行できます。
  4. リクエストを終了させる: 必要に応じて、リクエストの処理を終了し、レスポンスをクライアントに返します。このあとは次のミドルウェア関数が実行されることはありません。
const express = require('express');
const app = express();

// 1. ログ記録ミドルウェア
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // 次のミドルウェアへ進む
});

// 2. 認証ミドルウェア
app.use((req, res, next) => {
  if (!req.headers['x-auth-token']) {
    return res.status(401).send('Unauthorized: No token provided');
  }
  next(); // 認証成功の場合は次のミドルウェアへ進む
});

// 3. レスポンスヘッダー設定ミドルウェア
app.use((req, res, next) => {
  res.set('X-Powered-By', 'Express'); // カスタムヘッダーの設定
  next(); // 次のミドルウェアへ進む
});

// 4. リクエスト終了の例 (エラーハンドリング)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
  // 次のミドルウェアへ進まず、ここでリクエストを終了
});

// ルートハンドラ
app.get('/', (req, res) => res.send('Hello World!'));

// サーバーの起動
app.listen(3000, () => console.log('Server is running on port 3000'));

3-3.テンプレートエンジンの導入

テンプレートエンジンの選択

Expressでは、以下のようなテンプレートエンジンを利用できます。好みのものを選んでいただいて大丈夫だと思いますが、PUGやEJSが主流でしょうか?

EJS: JavaScriptの埋め込みをサポートし、動的なコンテンツ生成が簡単です。
Pug(旧Jade): 簡潔な構文でHTMLを記述できるテンプレートエンジン。
Handlebars: EJSに似ており、テンプレートの再利用や部分テンプレートが簡単にできます。

EJSで動的なコンテンツを生成

テンプレートファイルはデフォルトではアプリケーションのルートディレクトリ内のviewsディレクトリに配置することになります。

project-root/
├── views/
│   └── index.ejs
└── app.js (または index.js)
// app.js (または index.js)
const express = require('express');
const app = express();

// テンプレートエンジンとしてEJSを設定
app.set('view engine', 'ejs');

// ルートでEJSをレンダリングして動的なコンテンツを生成
app.get('/', (req, res) => {
  res.render('index', { title: 'Home', name: 'My name is...' });
});

// サーバーを起動
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
// views/index.ejs
<html>
<head><title><%= title %></title></head>
<body>
  <h1>Hello, <%= name %>!</h1>
</body>
</html>

3-4.エラーハンドリングとデバッグ

Expressでエラーハンドリングするには、エラーが発生したときにキャッチし、適切なレスポンスを返すために、4つの引数(err, req, res, next)を持つミドルウェアを作成します。

非同期関数(async/await)でエラーが発生する想定の場合には、try/catchを使い、エラーを適切に処理します。

const express = require('express');
const app = express();

// リクエストを解析するためのミドルウェア (例: JSON パーサー)
app.use(express.json());

// 通常のルート
app.get('/success', (req, res) => {
  res.send('This route works fine!');
});

// エラーを意図的に発生させるルート
app.get('/error', (req, res, next) => {
  const err = new Error('Something went wrong!');
  next(err); // エラーハンドリングミドルウェアへ渡す
});

// 非同期エラーハンドリングの例
app.get('/async-error', async (req, res, next) => {
  try {
    // 非同期処理 (例: データベースアクセスなど)
    throw new Error('Async error occurred!');
  } catch (err) {
    next(err); // エラーハンドリングミドルウェアへ渡す
  }
});

// 404エラーハンドリング: 存在しないルートへのリクエストをキャッチ
app.use((req, res, next) => {
  res.status(404).send('Sorry, the page you are looking for does not exist!');
});

// 汎用エラーハンドリングミドルウェア
app.use((err, req, res, next) => {
  console.error(err.stack); // エラースタックをコンソールに出力
  res.status(500).send('Something broke!'); // 500エラーを返す
});

// サーバーの起動
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

まとめ

他にも以下の理解があると開発中困らないかと思います。
余力がある方はぜひ検索してみてください。

  • セッション管理
  • DB接続
  • JWT認証
  • cors
  • 環境変数の管理

おすすめハンズオン集

https://qiita.com/tyoukan__/items/6888967ee61d7fb59115
https://dev.to/wizdomtek/typescript-express-building-robust-apis-with-nodejs-1fln
https://zenn.dev/hikar4215/articles/69e23611452e27

ハンズオンではないですが、各機能別にExpressの詳細を見たければこちらを...
git clone https://github.com/expressjs/express.git --depth 1
cd express
npm install
# 実行したいexampleを選択
node examples/content-negotiation

https://github.com/expressjs/express/tree/master/examples

  • auth - Authentication with login and password
  • content-negotiation - HTTP content negotiation
  • cookie-sessions - Working with cookie-based sessions
  • cookies - Working with cookies
  • downloads - Transferring files to client
  • ejs - Working with Embedded JavaScript templating (ejs)
  • error-pages - Creating error pages
  • error - Working with error middleware
  • hello-world - Simple request handler
  • markdown - Markdown as template engine
  • multi-router - Working with multiple Express routers
  • mvc - MVC-style controllers
  • online - Tracking online user activity with online and redis packages
  • params - Working with route parameters
  • resource - Multiple HTTP operations on the same resource
  • route-map - Organizing routes using a map
  • route-middleware - Working with route middleware
  • route-separation - Organizing routes per each resource
  • search - Search API
  • session - User sessions
  • static-files - Serving static files
  • vhost - Working with virtual hosts
  • view-constructor - Rendering views dynamically
  • view-locals - Saving data in request object between middleware calls
  • web-service - Simple API service

Discussion