30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Dockerfile に入門して Node.js アプリを作ってみる

Docker への入門の一環として、自分で Dockerfile を作成し、それを使って Node.js アプリを Docker Container で動かしてみる。

Hello World

Dockerfile を使うことで、既存の Docker Image を編集して新しい Docker Image を作ることができる。
具体的には、Dockerfileという名前のファイルにコマンドを記述していくことで、その内容に基づいた Docker Image を作成できるようになる。

例えば以下の Dockerfile では、FROMCMDというコマンドを使っている。これらのコマンドの意味は後述する。

FROM node:16
CMD [ "echo", "Hello World" ]

カレントディレクトリに上記のDockerfileがある状態で% docker build -t sample .を実行すると、sampleという名前の Docker Image が作成される。名前はもちろんsample以外でも構わない。
作られたsampleから Docker Container を作成し実行すると、Hello Worldが表示される。ちなみに--rmオプションを付けておくと、Docker Container の停止時にその Docker Container を自動的に削除してくれる。

% docker run --rm sample
Hello World

このように、

  1. Dockerfileというファイルにコマンドを記述する
  2. Dockerfileに基づいて Docker Image を作成する
  3. 作られた Docker Image から Docker Container を作成、実行する

というのが基本的な流れになる。

Dockerfile コマンドを知らないと意図した Docker Image は作れないので、まずはコマンドについて簡単に見ていく。

FROM

ゼロから Docker Image を作ることはまずなく、既存の Docker Image をベースして、そこにコマンドによるカスタマイズを重ねてオリジナルの Docker Image を作ることになる。
そのベースとなる Docker Image を指定するのが、FROMコマンド。

先程はFROM node:16と記述したが、このようにすると公式が配布している Node.js v16の Docker Image がベースになる。

CMD

これは、Docker Container の起動時に実行するコマンドを指定するためのコマンド。

そのため、以下のようにすると、Docker Container の起動時に Node.js のバージョンが表示される。

FROM node:16
CMD [ "node", "--version" ]

実際に試してみる。
まずは Docker Image の作成。

% docker build -t sample .

sampleという Docker Image は先程も作成したので既に存在するが、新しい内容でsampleという Docker Image が改めて作られるので、問題ない。
sampleから Docker Container を作成、実行すると、Node.js のバージョンが表示される。

% docker run --rm sample
v16.14.2

Docker Image の後片付け

上述したように、既に存在する Docker Image と同名の Docker Image を作成しようとした場合、特にエラーになることはなく、問題なく作成される。
ではこれまで存在した Docker Image はどうなるかというと、<none>という名前に変化する。
% docker imagesで調べてみると、<none>という名前の Docker Image が作られているはず。

この記事では基本的にsampleという名前で Docker Image を作っていく。
そうすると、<none>という名前の Docker Image がどんどん増えていく。
使うことのない Docker Image が増えても容量を圧迫するだけなので、適宜削除していく必要がある。

ひとつひとつ削除してもいいのだが、以下のコマンドを実行することで<none>という名前の Docker Image を一括で削除できる。

% docker rmi $(docker images -f "dangling=true" -q)

COPY

先程紹介したCMDコマンドは、Dockerfile にひとつしか書くことができない。
では、Docker Container の起動時に複数のコマンドを実行させたい場合は、どうすればいいのか。
シェルスクリプトを使えばよい。シェルスクリプトに実行させたいコマンドを書いておき、CMDコマンドでそのシェルスクリプトを実行することで、複数のコマンドを実行できる。

CMD ["bash", "shell.sh"]

だがそのためには、任意のシェルスクリプト(上記の例だとshell.sh)を、Docker Image に含めないといけない。
そういった時に使うのが、COPYコマンドである。
COPYコマンドを使うと、ホストにあるファイルを Docker Image にコピーすることができる。

まず、以下の内容のshell.shというファイルをカレントディレクトリに用意する。

#!/bin/bash

echo 1
echo 2
echo 3

次に、Dockerfile の内容を以下のようにして、% docker build -t sample .でビルドする。

FROM node:16
COPY shell.sh ./
CMD [ "ls" ]

ビルドされた Docker Image から Docker Container を起動するとlsコマンドが実行されるが、既存のディレクトリに加えてshell.shも存在していることが分かる。

% docker run --rm sample
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
shell.sh
srv
sys
tmp
usr
var

Dockerfile を以下のように書き換えてビルド、実行すると、shell.shの中身が実行される。

FROM node:16
COPY shell.sh ./
CMD ["bash", "shell.sh"]
% docker run --rm sample
1
2
3

COPYコマンドは、ひとつめの引数でホスト側のパスを指定し、ふたつめの引数で Docker Image のパスを指定する。

ホスト側のパスの基準となるのは、% docker build -t sample ..の部分。ここで基準となるパスを指定する。

例えば、カレントディレクトリにあるmaterialというディレクトリにa.txtb.txtc.txtがある場合、以下のどちらの組み合わせでも、3 つのファイルを Docker Image にコピーできる。

  • COPY ./material ./と記述し、% docker build -t sample .でビルドする
    • 基準となるパスが.であり、COPYコマンドの引数が./materialなので、./material内にあるファイルがコピーされる
  • COPY ./ ./と記述し、% docker build -t sample -f Dockerfile ./materialでビルドする
    • 基準となるパスが./materialであり、COPYコマンドの引数が./なので、./material内にあるファイルがコピーされる
    • デフォルトだとビルド時に指定されたパス(このケースだと./material)にある Dockerfile を探してしまうため、-fオプションで明示的に Dockerfile を指定している

では、Docker Image のパスの基準は、どのように決まるのか。それを指定するのが、WORKDIRコマンドである。

WORKDIR

WORKDIRコマンドで指定したディレクトリが、Dockerfile でコマンドを実行する際の基準になる。
COPYにしろCMDにしろ、WORKDIRで指定したディレクトリが基準になる。
指定したディレクトリが既存の Docker Image に存在しなかった場合、自動的に作成される。

例えば以下の内容でビルド、実行を行うと、/foo/barと表示される。/foo/barpwdコマンドを実行したためである。

FROM node:16
WORKDIR /foo/bar
CMD [ "pwd" ]

WORKDIRコマンドを使わなかった場合は、これまで見てきたように/が基準のディレクトリとなる。

RUN

Docker Image のビルド時に実行するコマンドを指定するためのコマンド。
CMDは Docker Container の起動時に実行するコマンドを指定するためのものなので、そこが異なる。
また、CMDと違って複数書くことができる。
RUNで実行するコマンドの基準となるディレクトリも、WORKDIRで指定したディレクトリになる。

Node.js でウェブアプリを作る

最後に Dockerfile の実践として、Node.js アプリが起動する Docker Container を作ることにする。
アプリはどんなものでも構わないのだが、TypeScript で書かれた、Hello Worldを返すだけのウェブアプリにする。

まず、Docker は意識せずにアプリを書いていく。

必要なライブラリをインストール。

% yarn init -y
% yarn add -D typescript ts-node-dev @types/node@16

tsconfig.jsonを用意。

{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext", "DOM"],
    "esModuleInterop": true
  }
}

index.tsを用意。

import http from "http";

http
  .createServer(function (_, res) {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Hello World\n");
  })
  .listen(3000);

package.jsonscripts"start": "ts-node-dev index.ts"を書き加える。

この状態で% yarn run startを実行するとウェブサーバが起動するはずなので、curl で動作確認してみる。

% curl http://localhost:3000
Hello World

問題なく動いているので、ここから、Docker で動かしていく。

Dockerfile

まずは Dockerfile を書く。

FROM node:16
WORKDIR /scripts

COPY package.json ./
COPY yarn.lock ./
COPY tsconfig.json ./

RUN npm i -G yarn
RUN yarn install --frozen-lockfile

COPY index.ts ./

CMD ["yarn", "run", "start"]

必要なファイルをコピーしたあと、ライブラリのインストールを行う。
その後index.tsをコピーし、Docker Container の起動時にyarn run startが実行されるようにする。

Docker Image のビルドと Docker Container の起動

ビルドは今まで通り% docker build -t sample .でよい。

Docker Container の起動は、以下のようにする。

% docker run --rm -d -p 8080:3000 sample

-dで、Docker Container がバックグラウンドで実行されるようにする。
そして-p 8080:3000で、ホスト側の8080ポートと Docker Container 側の3000ポートをマッピングする。
これで、ホストの8080ポートを通して、Docker Container の3000ポートにアクセスできる。

% curl http://localhost:8080
Hello World

最後に、このままだとずっと Docker Container が起動したままなので、% docker psで Docker Container で名前を調べ、% docker stop コンテナ名で停止させておく。

参考資料

Prisma に入門して API サーバを作ってみる

Prisma は、Node.js の ORM。
この記事では、導入方法、基本的な使い方について説明したのち、Prisma を使って簡単な API サーバを作ってみる。

Node.js のバージョンは16.13.2、MySQLのバージョンは8.0.28という環境で、動作確認している。

使用している npm ライブラリのバージョンは以下の通り。

  • @prisma/client@3.11.0
  • @types/node@16.11.26
  • prisma@3.11.0
  • ts-node-dev@1.1.8
  • typescript@4.6.2

MySQL のインストール

ORM である Prisma を試すためには、まずデータベースをセットアップしないといけない。今回は MySQL を使うことにする。
この記事では Homebrew を使ってインストールしているが、他の方法でももちろん問題ない。

% brew install mysql

バージョンを確認できればインストール成功。

% mysql --version
mysql  Ver 8.0.28 for macos11.6 on x86_64 (Homebrew)

brew services start mysqlで起動、brew services stop mysqlで終了。
以降、MySQL が起動しているという前提で話を進めていく。

以下のコマンドを打つといくつか質問されるので、root ユーザーのパスワード設定以外は全て No にする。Enter を押していけば No になるはず。

% mysql_secure_installation

以下のコマンドを打つと root ユーザーのパスワードを求められるので、先程設定したパスワードを入力すると、ログインできる。

% mysql --user=root --password

データベースの準備

MySQL にログインしたら、Prisma で操作するデータベースを用意する。
今回はprisma_dbという名前にしたが、特に制限はない。

mysql> CREATE DATABASE prisma_db;

SHOW DATABASES;で確認してprisma_dbが存在すれば成功。

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| prisma_db          |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

USE prisma_db;とすると、以降はprisma_dbに対して操作できるようになる。

SHOW TABLES;を実行すると、prisma_dbにはまだテーブルがないことを確認できる。

mysql> SHOW TABLES;
Empty set (0.00 sec)

最後に、このデータベースのポート番号を確認する。Prisma の設定時に必要になる。

mysql> show variables like 'port';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| port          | 3306  |
+---------------+-------+
1 row in set (0.00 sec)

Prisma プロジェクトを作成する

まずは必要なライブラリをインストールする。

% yarn init -y
% yarn add -D typescript ts-node-dev prisma

TypeScript で開発していくので、以下のtsconfig.jsonを作る。

{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext", "DOM"],
    "esModuleInterop": true
  }
}

以下のコマンドを実行すると、いくつかのファイルが生成される。

% yarn run prisma init

まず.env
このファイルでDATABASE_URLという環境変数が定義されているので、これを自身の環境に合わせて書き換える。
この記事の内容に沿って環境を構築した場合、以下のようになるはず。YOURPASSWORDの部分だけ、先程設定した root ユーザーのパスワードに書き換えればよい。

DATABASE_URL="mysql://root:YOURPASSWORD@localhost:3306/prisma_db"

そして、prisma/schema.prismaという名前のスキーマファイルも生成される。
以下の内容になっているはずなので、postgresqlの部分をmysqlに書き換える。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

モデルを定義する

環境構築が終わったのでここから実際に開発を進めていくが、まずはモデルを定義する。

先程編集したスキーマファイルに、モデルの定義を書き足していく。
スキーマファイルはPrisma Schema Language(PSL)という構文で書いていく。
また、.prismaファイルは、% yarn run prisma formatを実行することでフォーマットできる。

早速、Userというモデルを定義してみる。

model User {
  id      Int     @id @default(autoincrement())
  name    String
}

そして、以下のコマンドを実行する。

% yarn run prisma migrate dev --name init

そうすると様々な処理が行われるが、まず、@prisma/clientがまだインストールされていなかった場合はインストールされる。package.jsonに追記されていることを確認できる。
それから、prisma/migrationsディレクトリに、マイグレーションファイルが作成される。
そしてそのマイグレーションファイルをデータベースに対して実行する。

prisma_dbの中身を見てみると、Userテーブルが作られているのが分かる。

mysql> SHOW TABLES;
+---------------------+
| Tables_in_prisma_db |
+---------------------+
| _prisma_migrations  |
| User                |
+---------------------+

このように、Prisma のモデルはデータベースのテーブルに対応している。

DESCで調べてみると、モデルの定義通りに作成されている。

mysql> DESC User;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int          | NO   | PRI | NULL    | auto_increment |
| name  | varchar(191) | NO   |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

この時点ではレコードはまだ存在しない。

mysql> SELECT * FROM User;
Empty set (0.00 sec)

Prisma クライアントを使った CRUD 操作

Prisma クライアントを使ってコードを書いていくことで、データベースに対する操作を行うことができる。
ここでは、User テーブルに対して簡単な CRUD 操作を行っていく。

最初に、動作確認用の雛形となるコードを用意する。
src/index.tsという名前で以下のファイルを作成する。

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

async function main() {
  // ここにクエリを書いていく
}

main()
  .catch((e) => {
    throw e;
  })
  .finally(async () => {
    // データベースとのコネクションを切る
    await prisma.$disconnect();
  });

あとはmain関数のなかに具体的な処理を書き、% yarn run ts-node-dev src/index.tsで実行すればいい。

レコードの作成

レコードを作成するにはcreateメソッドを使う。createの返り値は、作成されたUserのオブジェクト。

async function main() {
  const user = await prisma.user.create({
    data: {
      name: "Alice",
    },
  });
  console.log(user); // { id: 1, name: 'Alice' }
}

% yarn run ts-node-dev src/index.tsを実行したあとにデータベースで検索を行うと、レコードが作られていることがわかる。

mysql> SELECT * FROM User;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
+----+-------+
1 row in set (0.00 sec)

dataに渡すフィールドがモデルの定義と一致していない場合、型エラーとなる。

// 以下は全て型エラー
async function main() {
  await prisma.user.create({
    data: {
      name: 1, // name の型が正しくない
    },
  });
  await prisma.user.create({
    data: {}, // name が渡されていない
  });
  await prisma.user.create({
    data: {
      name: 'Alice',
      bar: 'buz', // 不要なフィールド bar を渡している
    },
  });
}

実は、% yarn run prisma migrate dev --name initを実行した際にnode_modules内に型定義ファイルが自動生成されており、それを使って型チェックが行われている。

// node_modules/.prisma/client/index.d.ts から抜粋

/**
 * Model User
 * 
 */
export type User = {
  id: number
  name: string
}

idは auto_increment なので明示的に渡さなくてもよいのだが、もちろん渡してもよい。

async function main() {
  const user = await prisma.user.create({
    data: {
      id: 2,
      name: "Bob",
    },
  });
  console.log(user); // { id: 2, name: 'Bob' }
}

但し、既に存在しているidを渡してしまうと、プライマリキーとしての制約が守られていないので、実行時エラーになる。

async function main() {
  try {
    await prisma.user.create({
      data: {
        id: 2,
        name: "Carol",
      },
    });
  } catch (e) {
    // PrismaClientKnownRequestError
    // Unique constraint failed on the constraint: `PRIMARY`
    console.log(e);
  }
}

一度に複数のレコードを作成するときは、createManyを使う。

async function main() {
  const users = await prisma.user.createMany({
    data: [
      {
        name: "Carol",
      },
      {
        name: "Dave",
      },
    ],
  });
  console.log(users); // { count: 2 }
}
mysql> SELECT * FROM User;
+----+-------+
| id | name  |
+----+-------+
|  1 | Alice |
|  2 | Bob   |
|  3 | Carol |
|  4 | Dave  |
+----+-------+
4 rows in set (0.00 sec)

レコードの取得

レコードの取得を行うためのメソッドとしてここでは、findManyfindUniquefindFirstを紹介する。

まずfindMany。これは、複数のレコードを取得するメソッド。
条件を何も指定しなかった場合、全てのフィールドを持った全レコードが返ってくる。

async function main() {
  const allUsers = await prisma.user.findMany();
  console.log(allUsers);
  // [
  //   { id: 1, name: 'Alice' },
  //   { id: 2, name: 'Bob' },
  //   { id: 3, name: 'Carol' },
  //   { id: 4, name: 'Dave' }
  // ]
}

フィールドを指定するには、selectを使う。

async function main() {
  const users = await prisma.user.findMany({
    select: {
      name: true,
    },
  });
  console.log(users);
  // [
  //   { name: 'Alice' },
  //   { name: 'Bob' },
  //   { name: 'Carol' },
  //   { name: 'Dave' }
  // ]
}

レコードの絞り込みにはwhereを使う。
以下はcontainsを使って、「nameフィールドにoが含まれているレコード」を取得している。

async function main() {
  const users = await prisma.user.findMany({
    where: {
      name: {
        contains: "o",
      },
    },
  });
  console.log(users); // [ { id: 2, name: 'Bob' }, { id: 3, name: 'Carol' } ]
}

該当するレコードが存在しないときは空の配列を返す。

async function main() {
  const users = await prisma.user.findMany({
    where: {
      name: {
        contains: "z",
      },
    },
  });
  console.log(users); // []
}

contains以外にも様々な検索条件がある。詳しくは公式ドキュメントを参照。
https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#filter-conditions-and-operators

ORANDを使うこともできる。

// 「name が Al から始まる」 or 「id が 3 以上」
async function main() {
  const users = await prisma.user.findMany({
    where: {
      OR: [
        {
          name: {
            startsWith: "Al",
          },
        },
        {
          id: {
            gte: 3,
          },
        },
      ],
    },
  });
  console.log(users);
  // [
  //   { id: 1, name: 'Alice' },
  //   { id: 3, name: 'Carol' },
  //   { id: 4, name: 'Dave' }
  // ]
}
// 「name に o を含む」 and 「id が 3 以上」
async function main() {
  const users = await prisma.user.findMany({
    where: {
      AND: [
        {
          name: {
            contains: "o",
          },
        },
        {
          id: {
            gte: 3,
          },
        },
      ],
    },
  });
  console.log(users); // [ { id: 3, name: 'Carol' } ]
}

orderByを使って並べ替えを行うこともできる。

async function main() {
  const allUsers = await prisma.user.findMany({
    orderBy: {
      id: "desc",
    },
  });
  console.log(allUsers);
  // [
  //   { id: 4, name: 'Dave' },
  //   { id: 3, name: 'Carol' },
  //   { id: 2, name: 'Bob' },
  //   { id: 1, name: 'Alice' }
  // ]
}

findUniqueを使うと、ユニークな識別子を使って、該当するレコードを取得できる。
該当するレコードが存在しない場合はnullが返ってくる。

async function main() {
  let user = await prisma.user.findUnique({
    where: {
      id: 1,
    },
  });
  console.log(user); // { id: 1, name: 'Alice' }

  user = await prisma.user.findUnique({
    where: {
      id: 99,
    },
  });
  console.log(user); // null
}

ユニークではないフィールドを使おうとすると型エラーになる。

async function main() {
  const user = await prisma.user.findUnique({
    where: {
      name: "Alice", // 型エラー
    },
  });
}

findFirstは、最初に見つかったレコードを返す。

async function main() {
  const user = await prisma.user.findFirst({
    where: {
      name: {
        contains: "o",
      },
    },
  });
  console.log(user); // { id: 2, name: 'Bob' }
}
// orderBy で降順にしているので、{ id: 3, name: 'Carol' } が返ってくる
async function main() {
  const user = await prisma.user.findFirst({
    where: {
      name: {
        contains: "o",
      },
    },
    orderBy: {
      id: "desc",
    },
  });
  console.log(user); // { id: 3, name: 'Carol' }
}

レコードのアップデート

レコードのアップデートには、updateメソッドを使う。

async function main() {
  const user = await prisma.user.update({
    where: {
      id: 4,
    },
    data: {
      name: "David",
    },
  });
  console.log(user); // { id: 4, name: 'David' }
}

存在しないレコードをアップデートしようとすると、実行時エラーになってしまう。

// id が 99 のレコードの name をアップデートしようとしているが、該当するレコードは存在しないため、実行時エラーになる
async function main() {
  const user = await prisma.user.update({ // 実行時エラー
    where: {
      id: 99,
    },
    data: {
      name: "David",
    },
  });
  console.log(user);
}

レコードの削除

レコードの削除にはdeleteメソッドを使う。

async function main() {
  const user = await prisma.user.delete({
    where: {
      id: 4,
    },
  });
  console.log(user); // { id: 4, name: 'David' }
}

存在しないレコードを削除しようとすると、実行時エラーになってしまう。

// id が 99 のレコードを削除しようとしているが、該当するレコードは存在しないため、実行時エラーになる
async function main() {
  const user = await prisma.user.delete({ // 実行時エラー
    where: {
      id: 99,
    },
  });
  console.log(user);
}

マイグレーション

モデルの追加・変更を行う場合は、スキーマファイルを更新して、再びprisma migrateを行えばいい。
今回はUserモデルにisAdminフィールドを足すことにする。

model User {
  id      Int     @id @default(autoincrement())
  name    String
  isAdmin Boolean
}

しかしこの状態で% yarn run prisma migrate dev --name add-is-adminを実行すると、エラーになってしまう

Error:
⚠️ We found changes that cannot be executed:

  • Step 0 Added the required column `isAdmin` to the `User` table without a default value. There are 3 rows in this table, it is not possible to execute this step.

UserにはisAdminが必須になるように変更を行おうとしたが、既にUserテーブルにはレコードが 3 つあり、それらは当然isAdminを持っていないため、エラーになってしまう。
対応方法はいくつかあるが、今回はデフォルト値を設定して解決することにする。

スキーマファイルを編集し、isAdminにデフォルト値を設定する。

model User {
  id      Int     @id @default(autoincrement())
  name    String
  isAdmin Boolean @default(false)
}

再度% yarn run prisma migrate dev --name add-is-adminを実行すると、今度は上手くいく。

データベースを調べてみると、新しいモデルの定義が反映されていることを確認できる。

mysql> DESC User;
+---------+--------------+------+-----+---------+----------------+
| Field   | Type         | Null | Key | Default | Extra          |
+---------+--------------+------+-----+---------+----------------+
| id      | int          | NO   | PRI | NULL    | auto_increment |
| name    | varchar(191) | NO   |     | NULL    |                |
| isAdmin | tinyint(1)   | NO   |     | 0       |                |
+---------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

mysql> SELECT * FROM User;
+----+-------+---------+
| id | name  | isAdmin |
+----+-------+---------+
|  1 | Alice |       0 |
|  2 | Bob   |       0 |
|  3 | Carol |       0 |
+----+-------+---------+
3 rows in set (0.00 sec)

node_modules/.prisma/client/index.d.tsの型定義も更新されているので、引き続き型の恩恵を受けながら開発することができる。

/**
 * Model User
 * 
 */
export type User = {
  id: number
  name: string
  isAdmin: boolean
}

API サーバの構築

最後に実践として、Prisma を使った簡単な API サーバを構築してみる。

仕様は以下のようにする。

  • /userGETメソッドでリクエストすると、全てのUserが返ってくる
  • /user/{ID}GETメソッドでリクエストすると、idフィールドがIDUserが返ってくる
  • /userPOSTメソッドでリクエストすると、Userが新規作成され、そのUserが返ってくる

まず、Node.js の型定義ファイルをインストールする。

% yarn add -D @types/node@16

次に、src/db.tsという名前のファイルを作成する。このファイルでは、Prisma クライアントでデータベース操作を行う。

// src/db.ts
import { PrismaClient, User } from "@prisma/client";

const prisma = new PrismaClient();

export async function getUser(id: number): Promise<User | null> {
  return await prisma.user.findUnique({
    where: {
      id,
    },
  });
}

export async function getAllUsers(): Promise<User[]> {
  return await prisma.user.findMany();
}

export async function createUser({ name }: { name: string }): Promise<User> {
  return await prisma.user.create({
    data: {
      name,
    },
  });
}

次に、src/index.tsでサーバについて記述していくのだが、まずは「全てのUserの取得」にのみ、対応させる。

import http from "http";

import { getAllUsers } from "./db";

function resNotFound(res: http.ServerResponse) {
  res.writeHead(404, {
    "Content-Type": "application/json",
  });
  res.write(JSON.stringify({ message: "Not Found" }) + "\n");
  res.end();
}

function resData(res: http.ServerResponse, data: unknown) {
  res.writeHead(200, {
    "Content-Type": "application/json",
  });
  res.write(JSON.stringify(data) + "\n");
  res.end();
}

http
  .createServer(async (req, res) => {
    if (req.url === undefined || !req.url.startsWith("/user")) {
      resNotFound(res);
      return;
    }

    switch (req.method) {
      case "GET": {
        switch (true) {
          case /^\/user$/.test(req.url): {
            const allUsers = await getAllUsers();
            resData(res, { data: allUsers });
            break;
          }
          default: {
            resNotFound(res);
            break;
          }
        }
        break;
      }

      default: {
        res.writeHead(405, {
          "Content-Type": "application/json",
        });
        res.write(JSON.stringify({ message: "Method Not Allowed" }) + "\n");
        res.end;
        break;
      }
    }
  })
  .listen(8080);

% yarn run ts-node-dev src/index.tsでサーバが起動するので、早速リクエストしてみる。

% curl http://localhost:8080/user
{"data":[{"id":1,"name":"Alice","isAdmin":false},{"id":2,"name":"Bob","isAdmin":false},{"id":3,"name":"Carol","isAdmin":false}]}

問題なく全てのUserを取得できた。

次に、idを指定して特定のUserを取得できるようにする。

以下の変更を加えてサーバを起動し直す。

@@ -1,6 +1,6 @@
 import http from "http";

-import { getAllUsers } from "./db";
+import { getAllUsers, getUser } from "./db";

 function resNotFound(res: http.ServerResponse) {
   res.writeHead(404, {
@@ -33,6 +33,12 @@
             resData(res, { data: allUsers });
             break;
           }
+          case /^\/user\/[0-9]+$/.test(req.url): {
+            const id = Number(req.url.slice(6));
+            const user = await getUser(id);
+            resData(res, { data: user });
+            break;
+          }
           default: {
             resNotFound(res);
             break;

そうすると、個別のUserを取得できるようになる。

% curl http://localhost:8080/user/1
{"data":{"id":1,"name":"Alice","isAdmin":false}}
% curl http://localhost:8080/user/2
{"data":{"id":2,"name":"Bob","isAdmin":false}}
% curl http://localhost:8080/user/9
{"data":null}

最後に、Userを作成できるようにする。
以下の変更を加えてサーバを起動し直せばよい。

@@ -1,6 +1,7 @@
 import http from "http";
+import { URLSearchParams } from "url";

-import { getAllUsers, getUser } from "./db";
+import { getAllUsers, getUser, createUser } from "./db";

 function resNotFound(res: http.ServerResponse) {
   res.writeHead(404, {
@@ -10,6 +11,14 @@
   res.end();
 }

+function resBadRequest(res: http.ServerResponse) {
+  res.writeHead(400, {
+    "Content-Type": "application/json",
+  });
+  res.write(JSON.stringify({ message: "Bad Request" }) + "\n");
+  res.end();
+}
+
 function resData(res: http.ServerResponse, data: unknown) {
   res.writeHead(200, {
     "Content-Type": "application/json",
@@ -47,6 +56,33 @@
         break;
       }

+      case "POST": {
+        switch (true) {
+          case /^\/user$/.test(req.url): {
+            let data = "";
+            req.on("data", (chunk) => {
+              data += chunk;
+            });
+            req.on("end", async () => {
+              const params = new URLSearchParams(data);
+              const name = params.get("name");
+              if (name === null) {
+                resBadRequest(res);
+                return;
+              }
+              const user = await createUser({ name });
+              resData(res, { data: user });
+            });
+            break;
+          }
+          default: {
+            resNotFound(res);
+            break;
+          }
+        }
+        break;
+      }
+
       default: {
         res.writeHead(405, {
           "Content-Type": "application/json",

POSTメソッドでUserを作成できるようになっている。

% curl -d name="Dave" http://localhost:8080/user
{"data":{"id":5,"name":"Dave","isAdmin":false}}
% curl http://localhost:8080/user
{"data":[{"id":1,"name":"Alice","isAdmin":false},{"id":2,"name":"Bob","isAdmin":false},{"id":3,"name":"Carol","isAdmin":false},{"id":5,"name":"Dave","isAdmin":false}]}

今回は対応しなかったが、アップデートや削除も同じ要領で実装すればよい。