前回に引き続き、ビンゴゲーム用のAPIを作成します。
今回は以下のエンドポイントを作成します。
- POST /v1/users: ユーザー作成
- GET /v1/users: id指定すればユーザー情報取得、未指定なら全ユーザー情報取得
加筆していくにあたり、ファイルを分割し、クラス定義を別ファイルに移し、BingoCardクラス以外に、新たにServiceクラス、Userクラスを設けました。
import { v4 as uuidv4 } from 'uuid';
export interface Env {
DB: D1Database;
}
export class Service {
db: D1Database;
}
export class User {
card: BingoCard;
name: string;
id: string;
}
export class BingoCard {
numbers: number[][];
}
データベースからデータを取り出す役目はServiceクラスへ集約するという使い分けです。
では、記述していきます。
エンドポイントの追加
エンドポイントの構成は先にパスを評価する形としました。
その後、GETかPOSTかなどを判定します。
ここらへんは、今後も書きやすい書き方へ組み替えていくでしょう。
import { Env, Service, BingoCard, User } from './classes';
export default {
async fetch(request, env, ctx): Promise<Response> {
const service = new Service(env);
if (request.url.endsWith('/v1/users')) {
let data = "";
if (request.method === 'POST') {
/* ここを書く */
} else if (request.method === 'GET') {
/* ここを書く */
}
return new Response(
data,
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
if (request.url.endsWith('/v1/cards')) {
if (request.method === 'POST') {
const card = new BingoCard();
return new Response(
JSON.stringify({ card: card.getCard() }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
}
return new Response('Hello World!');
}
} satisfies ExportedHandler<Env>;
POST /v1/users実装
JSON形式でユーザー名が送付されてくるので取りだして、その名前を元にUserクラスのインスタンスを生成します。
生成したものをServiceクラスのinsertUser関数へ渡すのですが、ここでインスタンスを渡す必要性は必ずしもないかもしれません。
UserクラスとServiceクラスの結合を抑えるために、名前やIDを直接渡したほうがいいかも。
if (request.url.endsWith('/v1/users')) {
let data = "";
if (request.method === 'POST') {
const { name }: { name: string } = await request.json();
const user = new User(name);
service.insertUser(user);
data = JSON.stringify({ id: user.getId() });
} else if (request.method === 'GET') {
/* 略 */
最後に、生成した(付与した)IDをリクエスト元へ返して、おしまいです。
クラス部分は、Userクラスを以下のように実装します。
export class User {
card: BingoCard;
name: string;
id: string;
constructor(name: string) {
this.card = new BingoCard();
this.name = name;
this.id = uuidv4();
}
}
これだと、Userオブジェクトを組み立て直す度にビンゴカードが再生成されてしまいますから、後で直さないとならないですね。
Serviceクラスは下記の様にして、D1データベースへデータを登録します。
export class Service {
db: D1Database;
constructor(env: Env) {
this.db = env.DB;
}
async insertUser(user: User): Promise<void> {
await this.db.prepare('INSERT INTO users (id, name) VALUES (?, ?)')
.bind(user.getId(), user.name)
.run();
}
}
GET /v1/users実装
やり方が正しいかわかりませんが、request.bodyを直接フラグ代わりにしています。
id指定がされている場合とされていない場合で処理をわけていますが、Serviceクラスの対応メソッドを呼び出すところは変わりません。
} else if (request.method === 'GET') {
if (request.body) {
const { id }: { id: string } = await request.json();
const user = await service.getUser(id);
data = JSON.stringify(user);
}
else {
const users = await service.getUsers();
data = JSON.stringify(users);
}
}
以下はServiceクラスの実装です。
データベースからデータを取り出し、その情報を元にUserオブジェクトを再生成しています。(先に書いたとおり、ビンゴカード含めて)
async getUsers(): Promise<User[]> {
const { results } = await this.db.prepare('SELECT * FROM users').all();
return results.map((row) => {
const user = new User(row.name as string);
user.id = row.id as string;
return user;
});
}
async getUser(id: string): Promise<User> {
const { results } = await this.db.prepare('SELECT * FROM users WHERE id = ?').bind(id).all();
const user = new User(results[0].name as string);
user.id = results[0].id as string;
return user;
}
テーブル設計
テーブルは以下の通り作成しています。
DROP TABLE IF EXISTS Users;
CREATE TABLE IF NOT EXISTS Users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
INSERT INTO
Users (id, name)
VALUES
('aaaa-bbbb-cccc-dddd', 'Aaaa Bbbbb'),
('eeee-ffff-gggg-hhhh', 'Ccccc Ddddddd');
動作確認
おおよそ上記の記載でユーザー作成と情報取得ができるようになります。
リクエスト内容は下記のような感じです。
POST http://127.0.0.1:8787/v1/users
content-type: application/json
{
"name": "John fdsfdsfse"
}
###
GET http://127.0.0.1:8787/v1/users
###
GET http://127.0.0.1:8787/v1/users
{
"id": "eeee-ffff-gggg-hhhh"
}
終わりに
今回は、データベースから情報を取り出す度に、オブジェクトを再生成する方法をとっていますが、これが正攻法なのかはわかりません。
今後、設計上無理が出てきたら、何かしら変更したいと思っている、気にかかりポイントです。
では、引き続き作成していきたいと思います。
ここまで、読んでいただきありがとうございました。