前回の記事
はじめに
こんにちは、梅雨です。
前回に記事では、npm パッケージのインストールから始め、Express を用いてサーバを起動をするところまで学びました。
今回は、REST API という概念に触れ、リソースの作成、更新、取得、削除といった基礎的な機能を持つバックエンドの開発を行なっていきます。
CRUD とは?
ここではまず、バックエンドの機能を開発していく上で重要となる、「CRUD」というものから学んでいきましょう。
「CRUD」は
- Create (作成)
- Read (読み取り)
- Update (更新)
- Delete (削除)
の頭文字をとった言葉で、主にデータベースの基本的な処理操作を表す用語として用いられます。
前回作成したバックエンドでは単に「Hello World!」という文字列を表示するだけでしたが、実際のアプリケーションではデータベースに格納された情報を元にさまざまなデータを返却します。
データベースに対して CRUD 操作を行うときは、多くの場合SQLという言語を用いてデータのやり取りをします。
今ここで覚える必要はありませんが、例えば表に示すようなusers
テーブルがあった場合、SQL は以下のような構文で記述することができます。
id | name |
---|---|
1 | Miku |
2 | Teto |
# データ作成 (= Create)
$ INSERT INTO users (id, name) VALUES ('3', 'Meiyu');
# データ取得 (= Read)
$ SELECT * FROM users WHERE id = '3';
# データ更新 (= Update)
$ UPDATE users SET name = 'Tsuyu' WHERE id = '3';
# データ削除 (= Delete)
$ DELETE FROM users WHERE id = '3';
現在主流のデータベースにはPostgreSQLやMySQLなど SQL 準拠のものが多いですが、最近ではドキュメント型の MongoDB や、キーバリュー型の DynamoDB、インメモリ型の Redis なども人気です。
SQL 準拠のデータベースと対比して、これらのデータベースはNoSQLと呼ばれます。それぞれのデータベースでは得意なことが異なるため、用途に応じて使い分ける必要があります。
CRUD に対応する 4 つの HTTP メソッド
CRUD は主にバックエンドからデータベースへアクセスを行う際に特に意識することとなる処理操作でした。これらの操作に対応して、今度はクライアントからバックエンドにリクエストを送信していくことになります。
ここで覚えておいて欲しいのが HTTP メソッドと呼ばれるものです。
HTTP メソッドには 8 つのメソッドが定義されていますが、その中でも特に
- GET
- POST
- PUT
- DELETE
の 4 つは必ず覚えておきましょう。
これらのメソッドは CRUD 操作に対応しており、その対応は以下の表のようになっています。
HTTP メソッド | CRUD | 処理操作 |
---|---|---|
GET | Read | 読み取り |
POST | Create | 作成 |
PUT | Update | 更新 |
DELETE | Delete | 削除 |
これらのメソッドはどのようなときに使用するのでしょうか?
今回は例として前回作成したバックエンドに対し GET リクエストを送信してみましょう。
まずはサーバを起動します。
$ node server.js
次に、ターミナルウィンドウを新しく開いて以下のコマンドを実行しましょう。
$ curl -X GET http://localhost:3000
すると、前回ブラウザに表示されたのと同様の文字列が返ってきます。
Hello World!
curl コマンドは特定の URL に対して HTTP リクエストを送信することのできるコマンドになっています。-X
オプションの後ろにはリクエストのメソッドを指定することができ、今回は GET リクエストを送信しました。curl コマンドはデフォルトで GET リクエストを送信するので、省略しても大丈夫です。
我々が普段使うブラウザは、入力された URL を元に GET リクエストを送信します。そのため、前回ブラウザで http://localhost:3000 にアクセスした時と同じ文字列を取得することができました。
REST API とは?
ここで、新しく REST API という概念について学んでいきましょう。
そもそも API とは「Application Programming Interface」の略であり、あるシステムを外部から利用することのできる仕組みのことを指します。システム同士でどのように連携を行うかの仕様を事前に決めておくことで、さまざまな機能を別のシステムに取り込むことができます。
このような API のうち、Web 上で提供されるものを Web API と呼びます。Web API は HTTP / HTTPS 通信によって実現され、インターネットを経由してシステムを連携することができる仕組みとなっています。
Web アプリケーションを開発する際は、一般的にライブラリと API を駆使して開発を進めていくことになります。
例えば Zenn の Web API に対して、以下のリクエストを送信するとユーザの書いた記事の情報を取得することができます。
$ curl "https://zenn.dev/api/articles?username=tsuyuni"
{"articles":[{"id":338776,"post_type":"Article","title":"【TypeScript】Expressでバックエンドに入門する(基礎編)","slug":"d1dfba9345f499","comments_count":0,"liked_count":2,"bookmarked_count":0,"body_letters_count":6403,"article_type":"tech","emoji":"📙","is_suspending_private":false,"published_at":"2024-12-02T04:52:29.570+09:00","body_updated_at":"2024-12-02T04:52:29.570+09:00","source_repo_updated_at":"2024-12-02T04:52:29.569+09:00","pinned":false,"path":"/tsuyuni/articles/d1dfba9345f499","user":{"id":188376,"username":"tsuyuni","name":"梅雨","avatar_small_url":"https://lh3.googleusercontent.com/a/ACg8ocJ_lwuOBbK3LQJmgH-1LYexwGE_UlCvbo31gZz6iCwrANpLuZbz=s96-c"},"publication":null},{"id":338238,"post_type":"Article","title":"【TypeScript】Expressでバックエンドに入門する(導入編)","slug":"78207d8cf50d23","comments_count":0,"liked_count":1,"bookmarked_count":0,"body_letters_count":5816,"article_type":"tech","emoji":"📕","is_suspending_private":false,"published_at":"2024-12-01T03:11:16.509+09:00","body_updated_at":"2024-12-02T15:56:52.876+09:00","source_repo_updated_at":"2024-12-02T15:59:17.806+09:00","pinned":false,"path":"/tsuyuni/articles/78207d8cf50d23","user":{"id":188376,"username":"tsuyuni","name":"梅雨","avatar_small_url":"https://lh3.googleusercontent.com/a/ACg8ocJ_lwuOBbK3LQJmgH-1LYexwGE_UlCvbo31gZz6iCwrANpLuZbz=s96-c"},"publication":null}],"next_page":null}
一方で、さまざまな API が各々異なる仕様で設計されていたらそれを使用する側の負担はとてつもないものとなってしまいます。
それを避けるために、Web API では事前に定められた共通の仕様に基づいて API を設計することで、誰もが API にアクセスしやすいようにしています。
それらの仕様にはいくつかありますが、その中でも特によく用いられるものの 1 つに REST API と呼ばれるものがあります。
REST API の設計原則について詳しく説明するのは非常に大変なので本記事では割愛しますが、今後この記事では REST API の仕様に基づいてバックエンドの開発を進めていきます。
↓ 参考になる記事
前述の通り、REST API は外部のシステムから参照されることを前提とした仕様となっています。自分だけが使用するバックエンドにおいてなぜ REST API の思想を持ち込み必要があるのか?と思う方もいるかもしれません。
これは、REST API がただシステム間の連携を助けるだけではなく、システムの設計方針自体を指し示してくれる存在となるからです。REST API の仕様に則ることで、開発者は余計なことに頭を悩ませずに済むのです!
API 設計
それでは、実際にデータベースに対して CRUD 操作を行なうエンドポイントを作成していきましょう。
エンドポイントとは特定のリソースに対応する URL のことを指します。REST API において 1 つのリソースは 1 つのエンドポイントと紐づけられ、HTTP メソッドによって行われる CRUD 処理を決定します。
ここからは具体的な実装を交えて REST API の開発を体験してもらいます。
今回は例として猫の種類と特徴を管理できる 「猫 API」 を作成しましょう。
猫 API には CRUD 操作に基づいた以下の要件を設けます。
- 猫の種類と特徴のデータ一覧を取得できる (Read)
- 猫の種類と特徴の個別のデータを取得できる (Read)
- 猫の種類と特徴のデータを新しく作成できる (Create)
- 猫の種類と特徴のデータを変更できる (Update)
- 猫の種類と特徴のデータを削除できる (Delete)
データテーブルの構造はつぎのようになります。
ID | name | feature |
---|---|---|
1 | マンチカン | 足が短く活発 |
2 | ペルシャ | 毛が長く大人しい |
このシステムにおいて、猫の種類と特徴のデータ(= cat)をリソースとして扱うため、次のようなエンドポイントを定めましょう。REST API においては、リソースの複数形をエンドポイントの名前として用いるのが適切とされています。
しかし、個別の取得や変更、削除は特定のリソースを指定する必要があります。そのため、GET リクエスト(一覧)および POST リクエストは/cats
に、GET リクエスト(個別)、PUT リクエストおよび DELETE リクエストは/cats/1
のようにリソース名 + 一意の識別子(今回の場合は ID)に対して送信されるべきです。
任意の ID を:id
と表現すると、メソッドと URL の設計は以下のようになります。
メソッド | URL | 行う処理 |
---|---|---|
GET | /cats |
一覧の取得 |
GET | /cats/:id |
個別の取得 |
POST | /cats |
新規作成 |
PUT | /cats/:id |
変更 |
DELETE | /cats/:id |
削除 |
エンドポイントの実装
API の設計が決まったので、エンドポイントの実装に移っていきましょう。
まずは SQLite というデータベース管理システムを使ってデータベースを作成しましょう。
SQLite 自体はインストール不要で使うことができますが、ターミナルでコマンド操作をする際はコマンドラインツールのインストールが必要です。コマンドラインツールが入っているかどうかは以下のコマンドで確認できます。
$ sqlite3 --version
# 入っている場合(バージョンが表示される)
3.43.2 2023-10-10 13:08:14 1b37c146ee9ebb7acd0160c0ab1fd11017a419fa8a3187386ed8cb32b709aapl (64-bit)
# 入っていない場合
bash: sqlite3: command not found
コマンドラインツールが入っていなければ Homebrew などを用いてインストールしましょう。(インストール方法は使用しているパソコンによって異なります。)
$ brew install sqlite3
コマンドラインツールが用意できたら、データベースを作成していきます。まずは SQLite に接続します。
$ sqlite3
すると、ターミナルの先頭がsqlite>
という表示に変わるはずです。以下のコマンドでデータベースを作成しましょう。
sqlite> .open database.db
すると、プロジェクト内にdatabase.db
というファイルが作成されます。また、SQLite はこの新規作成されたデータベースに接続された状態になります。
SQLite はファイルベースのデータベースライブラリであり、データベース自体にサーバを必要としないため、小規模な開発や組み込みデバイスなどで利用されます。
データベースファイルを削除すればデータを全て消すことができるため、簡単なデータ管理に向いています。一方で本格的な Web アプリケーションで使用されることはあまりありません。
続けて、cats
テーブルを作成しましょう。必要なカラムは
- id
- name
- feature
の 3 つです。文末のセミコロンを忘れないようにしましょう。
sqlite> CREATE TABLE cats (id INTEGET PRIMARY KEY, name TEXT, feature TEXT);
これでテーブルの作成ができました。次に初期データを作成します。
sqlite> INSERT INTO cats (id, name, feature) VALUES ("1", "マンチカン", "足が短く活発"), ("2", "ペルシャ", "毛が長く大人しい");
試しに SELECT 文でデータ取得すると、うまくデータが作成されていることがわかります。
sqlite> SELECT * FROM cats;
1|マンチカン|足が短く活発
2|ペルシャ|毛が長く大人しい
sqlite のコマンドラインは.exit
で終了することができます。
sqlite> .exit
続いて、Express から SQLite のデータベースにアクセスするために、npm の splite3 パッケージおよび@types/sqlite3 パッケージをインストールしましょう。
$ npm i sqlite3
$ npm i -D @types/sqlite3
server.ts
を開き、Express から SQLite のデータベースにアクセスするためのコードを書いていきます。sqlite3 パッケージをインポートし、db インスタンスを生成します。
import * as express from "express";
+ import * as sqlite3 from "sqlite3";
const app = express();
+ const db = new sqlite3.Database("./database.db");
+
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
次に、/cats
に GET リクエストが送られた際の処理を追加します。
import * as express from "express";
import * as sqlite3 from "sqlite3";
const app = express();
const db = new sqlite3.Database("./database.db");
app.get("/", (req, res) => {
res.send("Hello World!");
});
+ app.get("/cats", (req, res) => {
+ db.all("SELECT * FROM cats", (err, rows) => {
+ res.send(rows);
+ });
+ });
+
app.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
トランスパイル後サーバを起動して http://localhost:3000/cats にアクセスするとデータの配列が返却されていることが確認できます。
$ tsc server.ts
$ node server.js
curl コマンドでも同様のレスポンスが確認できます。
$ curl http://localhost:3000/cats
[{"id":"1","name":"マンチカン","feature":"足が短く活発"},{"id":"2","name":"ペルシャ","feature":"毛が長く大人しい"}]
GET メソッド(個別)、POST メソッド、PUT メソッド、DELETE メソッドも同様に実装していきましょう。リクエストボディにアクセスする場合、app.use(express.json());
と設定する必要があります。
import * as express from "express";
import * as sqlite3 from "sqlite3";
const app = express();
+ app.use(express.json());
const db = new sqlite3.Database("./database.db");
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.get("/cats", (req, res) => {
db.all("SELECT * FROM cats", (err, rows) => {
res.send(rows);
});
});
+ app.get("/cats/:id", (req, res) => {
+ const { id } = req.params;
+ db.get("SELECT * FROM cats WHERE id = ?", id, (err, row) => {
+ if (row) {
+ res.send(row);
+ } else {
+ res.sendStatus(404);
+ }
+ });
+ });
+
+ app.post("/cats", (req, res) => {
+ const { name, feature } = req.body;
+ db.run(
+ "INSERT INTO cats (name, feature) VALUES (?, ?)",
+ [name, feature],
+ function () {
+ res
+ .setHeader("Location", `http://localhost:3000/cats/${this.lastID}`)
+ .sendStatus(201);
+ }
+ );
+ });
+
+ app.put("/cats/:id", (req, res) => {
+ const { id } = req.params;
+ const { name, feature } = req.body;
+ db.run(
+ "UPDATE cats SET name = ?, feature = ? WHERE id = ?",
+ [name, feature, id],
+ function () {
+ res.sendStatus(204);
+ }
+ );
+ });
+
+ app.delete("/cats/:id", (req, res) => {
+ const { id } = req.params;
+ db.run("DELETE FROM cats WHERE id = ?", id, function () {
+ res.sendStatus(204);
+ });
+ });
+
app.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
これで GET、POST、PUT、DELETE 全てのメソッドの実装ができました。再度トランスパイルしてサーバを起動しましょう。
$ tsc server.ts
$ node server.js
最後に curl コマンドでそれぞれのメソッドの確認をしていきます。
GET リクエスト
# 一覧を取得
$ curl http://localhost:3000/cats
[{"id":1,"name":"マンチカン","feature":"足が短く活発"},{"id":2,"name":"ペルシャ","feature":"毛が長く大人しい"}]
# 個別に取得
$ curl http://localhost:3000/cats/1
{"id":1,"name":"マンチカン","feature":"足が短く活発"}
POST リクエスト
$ curl http://localhost:3000/cats -X POST -H 'Content-Type:application/json' -d '{"name":"アビシニアン","feature":"筋肉質で遊び好き"}'
Created
# GETリクエストで確認
$ curl http://localhost:3000/cats
[{"id":1,"name":"マンチカン","feature":"足が短く活発"},{"id":2,"name":"ペルシャ","feature":"毛が長く大人しい"},{"id":3,"name":"アビシニアン","feature":"筋肉質で遊び好き"}]
PUT リクエスト
$ curl http://localhost:3000/cats/3 -X PUT -H 'Content-Type:application/json' -d '{"name":"アビシニアン","feature":"短毛で動きがしなやか"}'
# GETリクエストで確認
$ curl http://localhost:3000/cats
[{"id":1,"name":"マンチカン","feature":"足が短く活発"},{"id":2,"name":"ペルシャ","feature":"毛が長く大人しい"},{"id":3,"name":"アビシニアン","feature":"短毛で動きがしなやか"}]
DELETE リクエスト
$ curl http://localhost:3000/cats/3 -X DELETE
# GETリクエストで確認
$ curl http://localhost:3000/cats
[{"id":1,"name":"マンチカン","feature":"足が短く活発"},{"id":2,"name":"ペルシャ","feature":"毛が長く大人しい"}]
以上で GET、POST、PUT、DELETE それぞれのメソッドに対するエンドポイントの実装が完了しました。
一般的に、各リクエストに対するレスポンスのステータスは以下のようにするのが良いとされています。
- GET: 200 OK
- POST: 201 Created
- PUT: 200 OK または 204 No Content
- DELETE: 200 OK または 204 No Content
POST リクエストにおいては、レスポンスヘッダの Location に作成されたリソースの URI を含めることが一般的です。
これらのプラクティスは RFC というインターネット技術の標準的な仕様を示したドキュメントに記載されており、HTTP に関連する情報は RFC 9110 としてまとめられています。
おわりに
この記事では、データベースの CRUD 操作、REST API の概要、そして実際にエンドポイントの実装までを行いました。
次の記事では、npm スクリプトの使い方についての解説をしようと思います。
分からないところや間違っていると思う部分があった方は、是非コメントしていただければと思います。