Next.js + Express + TypeScript + PostgreSQL の WEBアプリをさくっと立ち上げてプロトタイプ開発をしよう
こんにちは、旅行プラットフォーム部の島本です。現在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などのコンポーネントライブラリを使うことで画面の作成も簡単にできます。
私が開発中の新サービスはいずれ公開できればと思います。
島本 晃平
2013年新卒入社。旅行プラットフォーム部所属。
苔を育て始めました。