🧩

node.jsを使って簡単なAPIサーバーを構築する方法(Express + TypeScript + Vite)

2024/12/17に公開

この記事はRUNTEQアドベントカレンダー シーズン3 の 15日目の記事です。
https://qiita.com/advent-calendar/2024/runteq

はじめに

現在フロントエンドエンジニアとして働いているゆず (@yuzunosk55)と申します。
RUNTEQ AdventCalendar 2024 に参加させていただきました。

最近はNode.jsを使ったバックエンド開発の学習を進めているので、今回の記事では簡単なAPIサーバーの構築方法について学んだことをまとめていこうと思います。
まだまだ未熟者のため、構築内容の不備や間違っている箇所など多数あると思いますが、コメントなどでご指摘いただけるとありがたいです。

この記事が、Node.jsを使ったバックエンド開発をしたい初学者さんの参考になれば嬉しいです。

記事の対象者

  • JavaScriptの勉強している方(TypeScriptを使っていますが、難しい使い方はしていないので多分大丈夫だと思います。)
  • Node.jsの勉強をしている方
  • Expressを使った、APIサーバー構築方法を知りたい方

記事内容

  • Viteのテンプレートを使ったプロダクトの雛形作成
  • インストールしているライブラリについて
  • CRUD実装(RESTAPI)
  • Rest Client を使った動作確認方法の紹介
  • express-validator を使ったリクエストバリデーション作成
  • それぞれの処理(ミドルウェア)を組み合わせる
    • ファイル分割
  • 今後、追加する機能について

記事で取り扱わない内容

  • JavaScript の記述方法
  • Node.js の解説
  • TypeScript の解説
  • Vite の詳細解説
    ほかにも細かい内容は書けないと思います

かんたんな予備知識

今回の構築記事内で使っている主な技術について簡単な解説をしてみます

Node.jsとは

Node.jsは、JavaScriptをブラウザ外で実行するための実行環境です。

もともとJavaScriptはブラウザ内で動作する言語でしたが、Node.jsが開発されたことで動かせる領域が拡大しサーバーサイドやCLI、デスクトップアプリケーションなど、さまざまな環境でJavaScriptを実行できるようになりました。

非同期I/Oという仕組みを採用しているのも特徴のひとつです。

他の言語だと、一つの作業が終わるまで次の作業を待ったりしますが、Node.jsは非同期I/Oの為、一つの作業完了を待たずに次の作業を始めることができます。

Node.jsをAPIサーバーとして選ぶ大きな理由にフロントエンドとバックエンドで同じ言語を使用できる点があると思います。
JavaScriptという初心者が学びやすい言語で、フロントエンドとバックエンドの両方を学ぶことができるのは効率的で、プログラミングを学び始めた初心者には良い点だと考えています。

Expressとは

Expressは、Node.jsのデファクトスタンダードフレームワークという位置づけになっている思います。(Node.jsのフレームワークは他にもありますが、まずはExpressから始めるのが良いと考えています。)

フレームワークを使わずにNode.jsだけでアプリケーションを作成すると、簡単なAPI設計でも記述量が多くなりがちです。(ただし、内部処理を理解するために、最初はNode.jsだけで構築してみるのも勉強になります。)

Expressを使うと、ルーティングやミドルウェア(リクエストとレスポンスの間で行う処理)の実装が簡単になります。Expressは非常にシンプルで拡張しやすいですが、自由度が高く柔軟な分、デフォルトで多機能ではありません。
そのため、自分のアプリケーション開発の知識がそのまま反映される構築になるというイメージです。

自分が知らない機能は、全く組み込まれないので、初心者向けとも言えますし、逆に知識がないと機能が不足することもあります。

TypeScriptとは

TypeScriptは、JavaScriptに「型」という仕組みを追加したプログラミング言語です。
JavaScriptをベースに機能を拡張した言語であるため、JavaScriptのコードはそのままTypeScriptでも使えます。

JavaScriptは柔軟な言語ですが、型がないために大規模なプロジェクトではバグが発生しやすく、コードの保守が難しくなることがあるようです。TypeScriptが生まれた背景には、型のないJavaScriptで大規模な開発を行う際の課題を解決したいというニーズがあったと考えられます。

TypeScriptは「型」を使うことでコードの安全性と可読性を高められます。また、VSCodeなどのエディタとの相性も良いため、開発体験がとても良くなります。他にも、実行前に型チェックが行えるので、多くのエラーを事前に発見できます。
実際に使っていると、これらのメリットが大きすぎてJavaScriptに戻れなくなってしまうほどです。

今注目されている言語の一つですし、初心者でもJavaScriptを学んだらそのままTypeScriptまで手を伸ばして学ぶのも良いと思います。

今回の構築でもTypeScriptを使用しています。そのため、TypeScriptを学んでいないとコードが読みづらくなっているかもしれません。

使用理由は開発体験の向上や、型を変数や関数に当ててバグを減らしたいという理由なので、TypeScriptが分からなくてもおそらく大きな問題はないはずです。

Viteとは

Viteは、モダンなフロントエンド開発のためのビルドツールです。

ビルドツールとは

JavaScriptを使ったフロントエンド開発における「ビルド」は、開発者が書いたソースコードを、ブラウザが理解できる形式(HTML, CSS, JavaScriptなど)に変換するプロセスを指します。

ビルドツールで行われることの例

  • ブラウザで動作するJavaScriptへのトランスパイル(コード変換)
  • ファイルの結合・圧縮
  • 画像の最適化
  • linterやformatterによるコード整形

ここでは、なぜこのようなビルドを行う必要があるかについては割愛しますが、JavaScriptの歴史などを調べると理解が深まるでしょう。

上記のさまざまな処理を行うツールがそれぞれ存在していたため、ツールごとのインストールとそれぞれの設定ファイルを作成する必要がありました。

正直初学者には何が何かわからず、かなり混沌とした状態でした。

Viteを使えば細かい設定を覚えなくて良い

これらの使い方や設定方法をすべて覚えるのは大変で、すぐに開発に着手できないことは個人的にかなりストレスでした。

Viteはこの問題を解決しており、一般的なユースケースにはデフォルトで設定してくれています。そのため、ツールごとの複雑な設定ファイルを書く必要がなく、すぐに開発を始めることができます。
非常に高速で軽量である点も嬉しいポイントです。

webpackなどの他のビルドツール使われている現場であれば、キャッチアップする必要がありますが、現在はまずViteから学ぶという方向性で良いのではないかと思います。

本文 APIサーバーの構築

ここから実際に行った手順となります

開発環境

  • M2 Mac sonoma 14.0
  • Homebrew 4.4.12
  • node 23.4.0
  • npm 10.9.2

Viteのテンプレートを使ったプロダクトの雛形作成

Viteは、基本的にReactなどを使ってシングルページアプリケーションを作成するためのツールですが、Node.jsなど他のバックエンド言語といっしょに構築する場合のテンプレートが用意されています。

この記事では、フロント部分まで実装していませんが、フロントの開発にReactを使いたいので、テンプレートを使って構築していきます。
沢山のテンプレートがあるので、興味がある方は色々試してみてください。

この記事で使ったテンプレート ... vite-express
私はvite-expressというテンプレートを使用しています。

下記コマンドを実行すると、flameworkとTypeScriptを使用するか聞かれるので、Reactとyesを選択してください。

npm create vite-express

> npx
> create-vite-express

✔ Project name … my-vite-express
✔ Select a framework › React
✔ Do you use TypeScript? … yes

✔ Done! You can start with:
 1. cd my-vite-express
 2. npm install
 3. npm run dev

Happy hacking! 🎉

説明通りに npm install 後、npm run dev を実行し下記のような画面が表示されたら大丈夫です。

ディレクトリ構成

雛形のディレクトリ構成は、このようになっています。
この記事で触れないclientディレクトリの中身は割愛します。

.
├── index.html
├── package-lock.json
├── package.json
├── public
├── src
│   ├── client
│   └── server
│       └── main.ts
├── tsconfig.json
└── vite.config.ts

tsconfig.jsonに設定を追加

この時点で各種設定ファイル(vite.config.ts, tsconfig.json, package.json)が作成されているため、すぐに開発を始めることができます。
ここでは、自動で作成されたtsconfig.jsonに項目を追加します。
(自分の好みの設定項目も含まれているので、割愛していただいても問題ないです。)

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "allowImportingTsExtensions": true, // <- ここから
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    } // <- ここまで追加
  },
  "include": ["src"]
}

allowImportingTsExtensions, baseUrl, pathsの3つを追加しています。

設定項目 説明
baseUrl 「.」と設定すると、import時のパスをtsconfig.jsonファイルのある階層(今回の場合rootディレクトリ)からのパスに揃えられる。
paths 指定パスで指定エイリアスで記述できるようにする(現場で慣れているので、この記述方法にしています。)
allowImportingTsExtensions Linterの影響か、ファイル保存時にtsファイルをimportしているにも関わらず拡張子が.jsに書き換えられたりするので、一旦入れています。

インストールしているライブラリについて

Dependencies 説明
express 本文前に解説したとおり。
express-validator Express用のバリデーションミドルウェア。後に使います。
react フロント側を実装する際に使うJavaScriptライブラリ。
react-dom ReactコンポーネントをブラウザのDOMにレンダリングするためのパッケージ。reactをインストールする時にはセットで入れる。
tsx TypeScriptとJSXをサポートするためのツール。tsxを使うとコンパイルステップを省略し、TypeScriptファイルを直接実行できるようになる。開発体験が向上する。
typescript 本文前に解説したとおり。
vite-express ViteとExpressを統合した今回のテンプレートを使うためのツール。
DevDependencies 説明
@types/express Expressの型定義ファイル。TypeScriptでExpressを使用するのに必要。
@types/node Node.jsの型定義ファイル。TypeScriptでNode.jsを使用するのに必要。
@types/react Reactの型定義ファイル。TypeScriptでReactを使用するのに必要。
@types/react-dom ReactDOMの型定義ファイル。TypeScriptでReactDOMを使用するのに必要。
@vitejs/plugin-react ViteでReactを使用するためのプラグイン。
nodemon Node.jsアプリケーションの開発中にファイルの変更を監視し、自動的にサーバーを再起動するツール。
vite 本文前に解説したとおり。

CRUD実装(RESTAPI)

REAT APIを実装していきます。

エントリーポイント

vite-express側で、サーバー起動するための最小構成ファイル(./src/server/main.ts)を作成してくれています。

import express from "express";
import ViteExpress from "vite-express";

const app = express();

app.get("/hello", (_, res) => {
  res.send("Hello Vite + React + TypeScript!");
});

ViteExpress.listen(app, 3000, () =>
  console.log("Server is listening on port 3000..."),
);

コード解説

初期コードの状態は、/helloというrouteが一つ作成されているのと、.listen()メソッドを使ってアプリケーションサーバーを起動させ、リクエスト待機状態にしています。

メソッド 説明
express() Expressアプリケーションを作成するメソッド。Expressの機能を使うために変数に収納する。
app.get(path, callback [, callback ...]) HTTPリクエストメソッドのGETを使い指定パスにルーティングするメソッド。
ViteExpress.listen(expressApp, port) ViteとExpressを統合した環境でのサーバー起動メソッド。内部的にexpress.listen()と同様の動作を行うと考えています。express.listen()は、Node.jsの標準モジュールhttpを使ってhttp.createServer(app).listen()しているのとほぼ同じようで、Expressの方が少しだけシンプルに書けます。

ディレクトリ作成

./src/server配下のディレクトリ構成はこのようにしました。

.
├── controller
├── validator
├── router
├── types
└── main.ts
ディレクトリ/ファイル 役割
controller ビジネスロジックを処理するためのファイルを格納。各ルートに対する処理を実装する場所。
router ルーティングの設定を行うファイルを格納。各エンドポイントに対するルートを定義。
types TypeScriptの型定義を格納。アプリケーション全体で使用するインターフェースや型を定義。
validator リクエストデータのバリデーションを行うファイルを格納。入力データの検証を行う。
main.ts アプリケーションのエントリーポイント。サーバーの起動や基本的な設定を行う。

アプリケーションの機能拡張

expressは、ミドルウェアを追加することで機能を拡張することができます。
このサーバーでは、基本的にクライアントからJSON形式のデータをリクエストで受け取って、JSON形式のデータを返すことを想定しています。

ルーティングで指定しているパスにリクエストが送られてきた場合、第一引数のreqにリクエストに関する情報が色々渡されてきます。
※ console.logなどで出力してみると、非常に多くのデータが含まれている事がわかります。

しかしリクエストで渡されてきたJSONデータそのままでは、サーバー側がデータを理解できずundefinedになってしまいます。

reqestの中身を確認してみる

エントリーポイントに以下のRoutingを追加します
requestで渡ってくる様々でデータをconsoleに出力するエンドポイントです。

app.post("/request", (req, res) => {
  console.log('Request Method:', req.method);
  console.log('Request URL:', req.url);
  console.log('Request Headers:', req.headers);
  console.log('Request Body:', req.body);
  res.send('レスポンスを返す');
});

コードが記述できたら、サーバーを起動し別のターミナルからcurlコマンドでリクエストを飛ばしてみます

 curl -X POST http://localhost:3000/request \
      -H "Content-Type: application/json" \
      -d '{"key1": "hoge", "key2": "fuga"}'

するとサーバーを起動している方のターミナルにログが流れるはずです
ログを確認すると、requestのデータが出力されています。しかしrequest.bodyの値はundefinedになっていることが分かります。

Request Method: POST
Request URL: /request
Request Headers: {
  host: 'localhost:3000',
  'user-agent': 'curl/8.1.2',
  accept: '*/*',
  'content-type': 'application/json',
  'content-length': '32'
}
Request Body: undefined ← データが理解できずundefinedになっている

ミドルウェアを使ってJSONをパースする

エントリーポイントのファイル(main.ts)のexpressアプリケーションを作成している行の下に、ミドルウェアを追加します。

const app = express();
// JSON形式のリクエストボディを解析するためのミドルウェアを設定
app.use(express.json());
メソッド/機能 説明
app.use(path, callback) path に指定したリクエストに対して行う処理を追加する。path は省略可能で、省略すると全てのリクエストに対して処理を実行する。ミドルウェアと呼ばれる。
express.json() 受信したリクエストのボディが JSON 形式である場合に、そのデータを自動的に解析し、req.body プロパティに JavaScript オブジェクトに変換して渡してくれる。これがない場合、undefined が入る。この処理をパースと呼ぶ

json用のパーサーを追加した状態で、もう一度curlコマンドを実行するとちゃんとJavaScriptのオブジェクト形式でデータが出力されることがわかります。

Request Body: { key1: 'hoge', key2: 'fuga' }

これでサーバーでJSON形式のデータを受け取れるようになりました。

ルーティングの作り方

RESTAPIを作るので、簡単なroutingを用意します。
expressのroutingは、大きく2種類書き方があります。

  1. 作成したexpressアプリケーションのCRUD系メソッドを使うパターン
  2. Routerオブジェクトを作成し、routeを追加していくパターン

今回は、Routerオブジェクトを作るパターンで構築します。
(個人的には、ひと目でrouting書いてると分かるので、こちらの方が良さそうに思います)

userRouterを作成する

基本的なCRUD操作用のディレクトリとファイルを作成します。

server/router/users/index.ts
import express from "express";

// Routerオブジェクトを作成する
const router = express.Router();

// expressには, 同名のHTTPメソッドが用意されている
router.get('/users', function(req, res))
router.get('/users/:id', function(req, res))
router.post('/users', function(req, res))
router.put('/users/:id', function(req, res))
router.patch('/users/:id', function(req, res))
router.delete('/users/:id', function(req, res))

export default router;

次にrouterをまとめるファイルをrouterディレクトリに作成します。
次のように記述することで、エントリーポイントではapiRoutesを呼び出すだけですべてのrouteを扱えるようになります。

server/router/index.ts
import express from "express";

// さっき作ったusersのrouterをimport
import usersRouter from "@/server/router/users/index.ts";

const apiRouter = express.Router();
// '/users'では、usersRouterが呼ばれる
apiRouter.use('/users', usersRouter);

export { apiRouter };

※ 追記12/19 … この親Routerを作成したら、userRouterのpathから/users部分を消しておいてください。そのままだと/api/users/users/:idというエンドポイントになってしまいます。

apiRouterを呼び出すだけで、routerディレクトリ配下のすべてのrouteが使えるようになる

server/main.ts
import { apiRouter } from "@/server/router/index.ts"; // ← import追加

const app = express();
app.use(express.json());
// '/api'でリクエストされた時に呼ぶミドルウェアとして登録する
app.use('/api', apiRouter) 

// 404エラー(上記のroute以外のpathは想定外のためエラーを返す)
app.use((_req, res) => {
  return res.status(404).json({ message: 'Invalid API route' });
})

ViteExpress.listen(app, 3005, () =>
  console.log("Server is listening on port 3005..."),
);

ミドルウェア 説明
404エラーを返すミドルウェア Expressではエントリーポイントの処理がファイルの上から下に読まれていきます。そのためapiRouterの次のミドルウェア処理に入ってくるという事は、想定していないrouteにアクセスされたという事になります。そこで、404ステータスを付与してエラーメッセージを返すようにしています。

コントローラーの作り方

次に作成したusersのエンドポイントにリクエストがあった際の処理を作っていきます。
コントローラーとrouterのパスは同じ方が探しやすいと思うので、揃えていきます。

DBはまだ使わないので、簡単にオブジェクトの配列を用意しています。
3つほど例を作成しました。

server/controller/users/index.ts
import { Request, Response } from "express";

// TypeScript用の型
import { User } from "@/server/types/controller/users.ts";
import { ApiController } from "@/server/types/common/index.ts";

// TODO: DBに接続してデータを取得するようにする
let users: User[] = [
  { id: 1, name: "John Doe", email: "john.doe@example.com", age: 20, gender: "male" },
  { id: 2, name: "Jane Doe", email: "jane.doe@example.com", age: 21, gender: "female" },
];

// GET /users で使う関数
const getUsers: ApiController = async (req: Request, res: Response) => {
  return res.json(users);
}

// GET /users/:id で使う関数
const getUserById: ApiController = async (req: Request, res: Response) => {
  const user = users.find((user) => user.id === Number(req.params.id));

  if (!user) {
    return res.status(404).json({ message: 'User not found' });
  }

  return res.json(user);
}

// POST /users で使う関数
const registerUser: ApiController = async (req: Request, res: Response) => {
  const newUser: User = {
    id: users.length + 1,
    name: req.body.name,
    email: req.body.email,
    age: req.body.age,
    gender: req.body.gender
  };

  users.push(newUser);
  return res.status(201).json(newUser);
}

export { getUsers, getUserById, registerUser };
メソッド 説明
res.json() Expressのレスポンスオブジェクトのメソッド。JavaScriptオブジェクトや配列を渡すとJSON形式に変換してクライアントに送信する。自動的にContent-Typeヘッダーをapplication/jsonに設定してくれる。
res.status() HTTPレスポンスのステータスコードを設定できるメソッド。設定しない場合200が返る。ステータスコード設定後に、他のレスポンスメソッド(例: res.jsonres.send)と組み合わせて使う。

routerでコントローラーを呼び出す

作成した処理をrouterで呼ぶように設定します。
routerのCRUD系メソッドの第二引数にコールバックを書くことができます。
ここに設定することで、reqとresオブジェクトも受け取ることができます。

server/router/users/index.ts
import express from "express";

import { getUsers, getUserById, registerUser } from "@/server/controller/users/index.ts";

// Routerオブジェクトを作成する
const router = express.Router();

// expressには, 同名のHTTPメソッドが用意されている
router.get('/', getUsers) // ← ここに設定
router.get('/:id', getUserById) // ← こことか
router.post('/', registerUser) // ← ここにも 
router.put('/:id', function(req, res))
router.patch('/:id', function(req, res))
router.delete('/:id', function(req, res))

export default router;

他の処理に関しては割愛するので、良ければ作成してみてください。

Rest Client を使った動作確認の紹介

APIが出来たら、動作確認してみましょう。
ここでは簡単にAPIの動作確認できる、Rest ClientというVSCodeの拡張機能を紹介だけします。

下記のようなファイルを作成するだけで手軽に動作確認ができます。
詳細な使い方については、GitHubのリポジトリを参照してみてください。

src/server/http/user.http
# 拡張機能 Rest Client の利用
# REST APIのリクエストを送信するためのツール
# 
# 使い方
# 1. 拡張機能 Rest Client(humao.rest-client) をVScodeでインストール
# 2. 下記の各リクエストの上に表示される Send Request を押すとリクエストが送信される
# 
# 参考
# https://github.com/Huachao/vscode-restclient

###
GET http://localhost:3000/api/users

###
GET http://localhost:3000/api/users/1

###
POST http://localhost:3000/api/users
content-type: application/json

{
  "name": "tomato kun",
  "email": "tomato@example.com",
  "age": 19,
  "gender": "male"
}

ファイルを作ると、それぞれのエンドポイントの上に Send Request という文字が表示されるのでクリックします。

すると、下記のようにレスポンスが返ってきます。
Postmanとかを使うほどでもない動作確認などに便利なのでお試しください。

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 82
ETag: W/"52-ly3MggM9h37xbZzwkPZeWAA3teY"
Date: Mon, 16 Dec 2024 14:47:29 GMT
Connection: keep-alive
keep-alive: timeout=5

{
  "id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com",
  "age": 20,
  "gender": "male"
}

express-validator を使ったリクエストバリデーション作成

CRUD処理を作成して動作確認まで出来たので、次はリクエスト時に渡されたデータを検証するバリデーションを作成していきます。

バリデーションにはexpress-validatorというライブラリを使います。
まずはインストールします

npm install express-validator

express-validatorの公式
https://express-validator.github.io/docs/

基本的な書き方

body, param, query というそれぞれのデータに対しての基本的な書き方を紹介します。

req.bodyのデータに対してバリデーションを書く場合

import { body } from 'express-validator';

body('name')
    .notEmpty()
    .withMessage('ユーザー名を入力してください')

URLパスに対するバリデーションを書く場合
api/users/:id こういうパス

import { param } from 'express-validator';

param('id')
    .isInt({ min: 0 })
    .withMessage('IDは0以上の整数である必要があります')

クエリパラメータに対してバリデーションを書く場合
例えばapi/users?page=2&limit=10というページネーションで使うようなクエリパラメータ

import { query } from 'express-validator';

query('page').optional().isInt({ min: 1 })
.withMessage('ページ番号は1以上の整数である必要があります'),
  
query('limit').optional().isInt({ min: 1 })
.withMessage('リミットは1以上の整数である必要があります')

使用しているメソッド紹介

簡易的ですが、このような意味合いのメソッドになります。
最初にbody,param,queryと書いてから、その後にメソッドチェーンで検証を連ねていきます。
最後にwithMessageをつけて、メッセージを返してあげましょう。

メソッド 説明
body リクエストボディの指定フィールドに対するバリデーションを行います。
notEmpty() フィールドが空でないことを確認します。
isLength() フィールドの文字数が指定された範囲内であることを確認します。
isEmail() フィールドが有効なメールアドレス形式であることを確認します。
isInt() フィールドが整数であることを確認します。
isIn() フィールドの値が指定された配列内のいずれかであることを確認します。
param URLパスの指定パラメータに対するバリデーションを行います。
query クエリパラメータの指定フィールドに対するバリデーションを行います。
optional() パラメータが存在する場合のみバリデーションを行うようになるので、必須でないデータの検証時に付与する。
withMessage() バリデーションが失敗した場合に返されるエラーメッセージを指定します。

メソッドの調べ方

express-validatorは内部的にvalidator.jsを使っているようなので、どんなメソッドがあるかを調べるときには、下記のリポジトリなど見に行くと良いと思います。
https://github.com/validatorjs/validator.js

それぞれの処理(ミドルウェア)を組み合わせる

最後にそれぞれのミドルウェアを組み合わせることで、拡張しやすくなる書き方を紹介します。

最終的に、userRouterはこのようになりました。
後にそれぞれ説明します。

server/router/users/index.ts
import express from "express";

import { getUsers, getUserById, registerUser, updateUser, updateUserName, deleteUser } from "@/server/controller/users/index.ts";
import { validateRequest } from "@/server/validator/helper.ts";
import { validateId } from "@/server/validator/common/index.ts";
import { basicUserValidation, updateUserValidation } from "@/server/validator/users/index.ts";
import { requestErrorHandler } from "@/server/controller/helper.ts";

const router = express.Router();

// routerはpath,validator,controllerの3つを設定する
router.get('/', requestErrorHandler(getUsers));
router.get('/:id', [...validateId, validateRequest], requestErrorHandler(getUserById));
router.post('/', [...basicUserValidation, validateRequest], requestErrorHandler(registerUser));
router.put('/:id', [...validateId, ...updateUserValidation, validateRequest], requestErrorHandler(updateUser));
router.patch('/:id', [...validateId, ...updateUserValidation, validateRequest], requestErrorHandler(updateUserName));
router.delete('/:id', [...validateId, validateRequest], requestErrorHandler(deleteUser));

export default router;

記述内容の解説

自分の書き方だと、
routerの第一引数がエンドポイント(path)
第二引数にバリデーションを行うミドルウェア
第三引数で、データを操作する処理(コントローラー部分)を渡しています。

これはexpressのRouter部分のドキュメントを参考にしています。
https://expressjs.com/ja/api.html#router

構文としては、下記のような感じです。

router.use([path], [function, ...] function)

第二引数のバリデーションで行っていることの解説

バリデーションは下記のように配列の中に書くと、それぞれが関数として呼び出されます。
これをスプレッド構文を使って展開して、routerに渡しています。

// 例
const basicValidation = [
  body('name').notEmpty().withMessage('ユーザー名を入力してください'),
  body('name').isLength({max: 60}).withMessage('ユーザー名は60文字以内で入力してください'),
  body('email').notEmpty().withMessage('メールアドレスを入力してください'),
]

validateRequestで行っていること
バリデーション部分の最後のミドルウェアでは、バリデーションエラーがあった場合にまとめてエラーメッセージを含めたレスポンスを返す処理を行っています。

server/validator/helper.ts
import { validationResult } from 'express-validator';
import { Request, Response, NextFunction } from 'express';

// バリデーション結果をチェックするミドルウェア
const validateRequest = (req: Request, res: Response, next: NextFunction) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  next();
};

export { validateRequest };

const errors = validationResult(req) バリデーション結果はvalidationResultにreqestオブジェクトを渡すと取得できます。

データ処理部分の解説

router.crud()の第三引数では、データの処理を行っています。
コントローラーに渡すだけでも良いのですが、想定外のエラーが発生すると困るので例外処理をキャッチしてくれる関数を作成し、そこにコントローラーの関数を渡しています。

server/controller.helper.ts
import { Request, Response } from "express";

import { ApiController } from "@/server/types/common/index.ts";

// コントローラーを受け取る
const requestErrorHandler = (controller: ApiController) =>{
  return async (req: Request, res: Response) => {

   // コントローラー実行中に例外処理が発生したら, 500番とエラーメッセージを返す
    try {
      return await controller(req, res);
    } catch (error) {
      return res.status(500).json({ message: 'Internal Server Error' });
    }
  }
}

export { requestErrorHandler };

try...catch文の中で、controllerを実行することで例外処理を書くことができます。
このような包括的な関数を用意することで、コントローラー一つ一つに例外処理を書く必要がなくなるので、便利だとおもいます。

今後、追加したい機能について

ここまで読んでいただきありがとうございます。
これで、ある程度ファイル分割を行った上でAPIサーバーを構築していく準備ができました。

今後追加いきたいのは、

  • PostgreSQL × Prismaを使ってのDB連携
  • testを書く
  • フロントとの連携
  • Docker化、デプロイ

という感じです。
余裕があれば、また記事にしてみたいと思います。

最後に

今回はNode.js, Expressの使い方を学んだのでAPIサーバーを構築してやり方をまとめてみました。まだまだ未熟な技術力、知識ですので不足している内容も多々あると思います。
もし良ければ、コメントなどで教えていただけると幸いです。

同じような構成でAPIサーバーを構築してみたい方の助けになれば幸いです。
お読みいただきありがとうございました。

参考にさせていただいた記事・サイト

https://developer.mozilla.org/ja/docs/Learn/Server-side/Express_Nodejs/Introduction

https://qiita.com/baby-degu/items/860b78d2d1cd4894bc02

https://qiita.com/tomada/items/9eb9fac35423422d1c4e

https://expressjs.com/ja/api.html

https://github.com/Huachao/vscode-restclien

https://www.tohoho-web.com/ex/express.html#express.router

Discussion