🚄

Next.js+Express+TypeScript+PostgreSQLのWEBアプリをさくっと立ち上げてプロトタイプ開発をしよう

2020/12/20に公開

FORCIAアドベントカレンダー2020 20日目の記事です。

こんにちは、旅行プラットフォーム部の島本です。現在B2C向けの新サービス立ち上げを企てています。

新規事業立ち上げのプロセスの一つにプロトタイプ作成があります。

フォルシアには社内製のWEBアプリケーションフレームワーク(2019年に新フレームワークを開発しています)があるのですが、プロトタイプ作成のような信頼性よりスピード性重視の場合には、オーバースペック感があります。
一方で、世の中にはコマンドをいくつか実行するだけでWEBアプリを立ち上げられる便利なツールもあります。普段私が扱っているNode.jsベースのものだとこれらが挙げられます。

しかし、商用化を見据えると、今後すべて作り変えるであろうプロトタイプといえど、なるべく自社フレームワークに近い構成で開発したい気持ちもあります。 そこで下記の要素を取り入れたプロトタイプ用WEBアプリのベースをさくっと作ってみることにしました。

  • Next.js + Expressのカスタムサーバの構成
  • TypeScriptで開発
  • Backends For Frontends(BFF)構成っぽくする
  • DB参照も有り(フォルシアではPostgreSQLを利用することが多いため pg-promise を利用)

このような構成になります。

プロジェクトの作成

npx create-next-app [project-name]
cd [project-name][project-name]をnext_prototypeとして作成する

TypeScriptで開発するための設定

npm install -D typescript @types/react @types/react-dom @types/node
 
mv pages/index.js pages/index.tsx 
mv pages/_app.js pages/_app.tsx

pages/_app.tsxを編集しTypeScript化。

import { AppProps } from 'next/app'
 
const MyApp = ({ Component, pageProps }: AppProps) => {
	return ;
}
export default MyApp

起動できることを確認。

npm run dev

起動後、下記のtsconfig.jsonが作成される。

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

Expressのカスタムサーバを導入

Next.jsはデフォルトではパスと一致するpagesディレクトリ配下の各ファイルにルーティングされます。 このルーティングに独自実装を組み込みたい場合にカスタムサーバを利用します。 例えば、特定のパスの場合のCookie操作やリダイレクト処理の実装などが挙げられます。
参考: https://nextjs-ja-translation-docs.vercel.app/docs/advanced-features/custom-server

npm install express
npm install -D @types/express    
 
mkdir server
touch server/index.ts
touch tsconfig.server.json

server/index.tsを編集。

import express, { Request, Response } from "express";
import next from "next";
 
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const port = process.env.PORT || 3000;
 
app.prepare().then(() => {
	const server = express();
 
	server.all("*", (req: Request, res: Response) => {
		return handle(req, res);
	});
	server.listen(port, (err?: any) => {
		if (err) throw err;
		console.log(
                      `> Ready on localhost:${port} - env ${process.env.NODE_ENV}`
                );
	});
});

tsconfig.server.jsonを編集。

// for Next custom-server
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs", // Next.jsとExpressの両方を連携させるために、commmonjsを利用
    "outDir": "./dist",
    "noEmit": false, // Next.jsはBebelを使用してTypeScriptをコンパイルするので、TSコンパイラはjsを出力しない。設定を上書きする。
  },
  "include": ["./server"]
}

package.jsonのscriptを書き換える。

  "scripts": {
    "dev": "tsc -p tsconfig.server.json && node ./dist/index.js",
    "build:next": "next build",
    "build:server": "tsc -p tsconfig.server.json",
    "start": "NODE_ENV=production node dist/index.js"
  },

起動できることを確認。

npm run dev

pg-promiseを用いてDataBaseを参照できるようにする

PostgreSQLのインストールは割愛します。

npm install pg-promise @types/pg-promise
 
touch modules/database.ts

modules/database.tsを編集。

import pgPromise from "pg-promise";
 
const pgp = pgPromise({});
const config = {
	db: {
		// 設定項目: https://github.com/vitaly-t/pg-promise/wiki/Connection-Syntax
		host: "127.0.0.1",
		port: 5432,
		database: "mydb",
		user: "user",
		password: "password",
		max: 10, // size of the connection pool
		query_timeout: 60000 // 60sec
	}
};
 
export const sqlExecuter = pgp(config.db);

Next.jsのdevelopment modeでの起動中にソースを修正してrebuildが走るとpg-promiseの下記のWarningが発生します。

WARNING: Creating a duplicate database object for the same connection.

production mode では問題ありませんが、気になる場合は Singleton pattern でコネクションを再利用するよう実装することで回避できます。

詳しくはこちら:https://github.com/vercel/next.js/discussions/18008

DBから取得したデータを返すAPIのエンドポイントを追加

Next.jsでは pages/api 配下のファイルが、/api/* でアクセス可能なAPIエンドポイントとして扱われるため、ファイルを配置するだけでAPIエンドポイントを作成できます。

参考: https://nextjs.org/docs/api-routes/introduction

touch pages/api/data.ts

pages/api/data.ts を編集。

import { sqlExecuter } from "../../modules/database";
 
export default async (req: any, res: any) => {
 
	const data = await sqlExecuter.any(
               "select 'DB参照したデータ' as any_column"
        );
	res.status(200).json({
		data
	});
};

APIをfetchして取得したデータを画面に書き出す

npm install axios
touch modules/request.ts

modules/request.tsを下記の通りに編集。

import axios from "axios";
const serverSideBaseURL = "http://localhost:3000/api";
const clientSideBaseURL = "http://localhost:3000/api";
 
const requestInstance = axios.create({
	baseURL: serverSideBaseURL
});
 
const clientRequestInstance = axios.create({
	baseURL: clientSideBaseURL
});
 
export const getRequestInstance = (isServerSide: boolean) => {
	if (isServerSide) {
		return requestInstance;
	}
	return clientRequestInstance;
};

※Next.jsのgetInitialPropsの処理はサーバ側・クライアント側のいずれでも実行されるため、APIのパスが変わる場合の考慮が必要です。

pages/index.tsxを編集。

import { NextPage } from 'next'
import { getRequestInstance } from "../modules/request";
 
const Page: NextPage = ({ data }) => {
   return data.map(
      (d: any, index:number) => <div>{index}番目のデータ: {d.any_column}</div>
   )
}
Page.getInitialProps = async (ctx: any) => {
	const request = getRequestInstance(Boolean(ctx.req));
	const res = await request.get("data").then(res => res);
	return res.data;
}
export default Page

ブラウザで http://localhost:3000 にアクセスし画面にDB参照して得られたデータが表示されることを確認。

おしまい。

終わりに

いかがだったでしょうか?APIやDBのクライアントもインストールするだけですぐに使えて便利な世の中ですね。今回はAPIのレスポンスをそのまま書き出すまでで終わりましたが、Material UIなどのコンポーネントライブラリを使うことで画面の作成も簡単にできます。

私が開発中の新サービスはいずれ公開できればと思います。

FORCIA Tech Blog

Discussion