プログラミング

[開発記録]TypeScript+Express+Postgresqlで、簡易的なAPIサーバを構築した話④ -手続き型でGETリクエストを実装した話-

「TypeScript Express PostgreSQL」4本目です。
ようやく、実装に入りました。今回はとりあえず全体像をつかむため、GETリクエストができるところまで進めました。

実装の流れ

こんな感じで実装を進めていきます。最初から優れたアーキテクチャを構成するのではなく、まず動くものを作り、そのあとリファクタリングでシステムをきれいにしていきます。第2回でソース配置を提示しましたが、最終的な配置はより整備されたものになっていきます。

  1. 手続き型で実装
  2. オブジェクト指向型にリファクタリング
  3. DDD的にリファクタリング

手続き型で実装

Main Application実装

src直下のapp.tsはアプリケーションが実行されたタイミングで実行されるファイルです。
ここではExpressの実行のみ実施し、ユーザのリクエストを受け取り、API処理にリクエストを引き渡せるようにします。

app.ts

import express, { Request, Response, NextFunction } from "express";
import userRoutes from "./applications/routes/user";
import { json } from "body-parser";

// Express定義 JSONパーサー設定
const app = express();
app.use(json());
app.use(express.urlencoded({ extended: true }));

//CROS対応
app.use(
  (req: express.Request, res: express.Response, next: express.NextFunction) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Methods", "*");
    res.header("Access-Control-Allow-Headers", "*");
    next();
  }
);

// Route設定
app.use("/", userRoutes);

// サーバエラー実装
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  res.status(500).json({ message: err.message });
});

// APIサーバオープン
app.listen(3000, () => {
  console.log("Start on port 3000.");
});

Routing実装

application/routes/index.tsでは、ルーティングを実装します。ルーティングは、言うまでもないですがAPIリクエストとAPI処理を結びつける処理です。ユーザがリクエストしたパスに対応するAPI処理を呼び出します。

application/routes/index.ts

import { Router } from "express";

// domain/controllers/userで実装したAPI処理をインポート
import { getCustomers, postCustomer } from "../../domain/controllers/user";

// Router設定
const router = Router();

// API処理を登録
router.get("/users", getCustomers); // get customer
router.post("/users", postCustomer); // put customer

export default router;

Controller実装

domain/controllers/user.tsでは、APIの処理を実装します。実装中のものなので不格好ですが、APIリクエストを受けた都度でDBに接続を行い、CRUD的な処理を実施します。今はPOST処理も実装できていませんね。

今はDBへの接続をこのファイルで実装していますが、「③DDD的にリファクタリングする」、ではDBへの接続はinfrastructure側に移動する予定です。

domain/controllers/user.ts

import { RequestHandler } from "express";
import {
  createConnection,
  CustomRepositoryCannotInheritRepositoryError,
  getRepository,
} from "typeorm";

import dbConfig from "../../infrastructure/db-config";
import User from "../entities/user";

// ユーザ一覧取得 GET /users
export const getCustomers: RequestHandler = (req, res, next) => {
  console.log("API[getCustomers] START");
  // PostgreSQL に接続を行い。顧客情報を取得する。
  createConnection(dbConfig)
    .then(async (connection) => {
      console.log("PostgreSQL Connected");

      try {
        const customerRepository = getRepository(User);

        const allCustomers = await customerRepository.find();
        res.status(200).json({ response: allCustomers });
        console.log("Select : ", allCustomers);
      } catch (error) {
        console.error("Failed : ", error);
        res.status(404).json({ message: "データの取得に失敗しました。" });
      } finally {
        await connection.close();
        console.log("Connection Closed");
      }
    })
    .catch((error) => {
      console.error("PostgreSQL Connection Failed", error);
      res.status(500).json({ message: "DBの接続に失敗しました。" });
    });
};

// ユーザ登録 POST /users
export const postCustomer: RequestHandler = (req, res, next) => {
  console.log("API[postCustomers] START");
  // PostgreSQL に接続を行い。顧客情報を取得する。
  createConnection(dbConfig)
    .then(async (connection) => {
      console.log("PostgreSQL Connected");

      try {
        const customerRepository = getRepository(User);

        const allCustomers = await customerRepository.find();
        res.status(200).json({ response: allCustomers });
        console.log("Select : ", allCustomers);
      } catch (error) {
        console.error("Failed : ", error);
        res.status(404).json({ message: "データの取得に失敗しました。" });
      } finally {
        await connection.close();
        console.log("Connection Closed");
      }
    })
    .catch((error) => {
      console.error("PostgreSQL Connection Failed", error);
      res.status(500).json({ message: "DBの接続に失敗しました。" });
    });
}

Entity実装

domain/entities/user.tsはUserクラスを定義します。UserクラスはDBで管理するため、Typeormで利用する形で定義しています。

domain/entities/user.ts

import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export default class User {
  @PrimaryGeneratedColumn()
  public id!: number;

  @PrimaryColumn({ type: "varchar", length: 10 })
  public name: string;

  @Column({ type: "int" })
  public age: number = 0;

  constructor(name: string) {
    this.name = name;
  }
};

Infrastructure実装

infrastructure/db-config.tsはDBへの接続設定を定義します。この設定をControllersで利用し、CRUD処理をDBを使って利用できるようにします。entitiesではtypescriptからコンパイルしたJavascriptファイルを指定する必要があります。私はtsファイルを指定していて、トラブルシュートに2時間要しました。

infrastructure/db-config.ts

import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";
const dbConfig: PostgresConnectionOptions = {
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "admin", //実際のアプリでは、process.envから利用
  password: "admin", //実際のアプリでは、process.envから利用
  database: "admin",
  synchronize: true,
  logging: false,
  entities: ["dist/domain/entities/*.js"],
};
export default dbConfig;

【テスト】APIリクエスト

お試しでAPIリクエストしてみます。PostgreSQLのDockerイメージを実行したうえで、サーバを起動し、リクエストします。

$ curl -s http://localhost:3000/users
> {"response":[]}

データがまだ入っていないので、レスポンスは空になります。
ただ、TypeORMのsynchronizeをONにしているので、DBにテーブルが作成されています。

admin=# \dt
       List of relations
 Schema | Name | Type  | Owner 
--------+------+-------+-------
 public | user | table | admin
(1 row)

admin=# \d user
 id     | integer               |           | not null | nextval('user_id_seq'::regclass)
 name   | character varying(10) |           | not null | 
 age    | integer               |           | not null | 

まとめ

中途半端な状態ですが、APIのGETリクエストを実装できました。次回は、CRUD処理をさらに整備し、ユーザ登録、削除、更新を実装します。その後、ログ処理、エラー処理を整備し、オブジェクト指向型にリファクタリングしていきます。

以上、次回に続きます。