フロントエンドとバックエンドの連携とデプロイ
フロントエンドとバックエンドを連携する
Viteを用いて構築されたフロントエンドと、Node.jsを用いて構築されたバックエンドを接続する方法について学びましょう。
データベースの節で作成した掲示板サービスの処理の流れは、次の図のようになっていました。1つのWebサーバーが、フロントエンドで動作するアプリケーションの配信と、バックエンドで動作するアプリケーションの処理の両方を担っています。
今回作成する掲示板サービスでは、フロントエンドとバックエンドでそれぞれ別のサーバーを構築します。処理の流れは、次の図のようになります。
バックエンドを構築する
次の手順に従って、バックエンドを構築しましょう。
プロジェクトを格納するディレクトリを作成する
新しくプロジェクトを格納するディレクトリを作成して開きます。その中にbackendディレクトリを作成します。
TypeScriptのセットアップをする
-
カレントディレクトリを
backendディレクトリに移動し、npm initコマンドを実行してpackage.jsonファイルを作成します。 -
npm install -D typescriptコマンドを実行してTypeScriptをインストールし、npx tsc --initコマンドを実行してTypeScriptの設定を記述するためのtsconfig.jsonファイルを作成します。 -
ここでは、事前にTypeScriptをJavaScriptにトランスパイルせずにTypeScriptファイルを実行するために、次のコマンドを実行して
tsxパッケージをインストールします。npx tsx 実行するファイルのパスとすることで、TypeScriptファイルを実行できます。npm install -D tsx
データベースと開発用サーバーを作成する
データベースの節と同じように、データベースを作成し、Expressを用いて開発用サーバーを作成します。詳細は、データベースの節を参照してください。
-
Supabaseで新しいデータベースを作成します。
-
npm install @prisma/client @prisma/adapter-pg pg dotenvコマンドとnpm install -D prisma @types/pgコマンドを実行して、Prismaを利用するために必要なパッケージをインストールします。 -
npx prisma initコマンドを実行して、Prismaを利用するために必要なファイルを作成します。 -
.envファイルを編集し、Prismaがデータベースに接続できるようにします。 -
作成された
schema.prismaファイルを編集し、掲示板の投稿を保存するためのテーブルの定義を次のように記述します。backend/prisma/schema.prisma// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}
model Post {
id Int @id @default(autoincrement())
message String
} -
npx prisma db pushコマンドを実行してテーブルの定義をデータベースに反映します。 -
npx prisma generateコマンドを実行してPrismaClientクラスなどを生成します。 -
掲示板の投稿のサンプルデータをデータベースに登録します。
-
npm install expressコマンドとnpm install -D @types/expressコマンドを実行して、Expressとその型定義をインストールします。 -
main.tsファイルを作成し、次のように記述します。backend/main.tsimport express from "express";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client.js";
const app = express();
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
const client = new PrismaClient({ adapter });
app.use(express.json());
app.get("/posts", async (request, response) => {
const posts = await client.post.findMany();
response.json(posts);
});
app.post("/posts", async (request, response) => {
await client.post.create({ data: { message: request.body.message } });
response.sendStatus(201); // Created(新しいメッセージを作成)
});
app.listen(3000);
CORSに関する設定をする
CORSに関する設定を行います。
プロトコル、ドメイン、ポートの組み合わせのことをオリジンと呼びます。
異なるオリジンのリソースへアクセスすることはブラウザによって制限されることがあります。フロントエンドの開発用サーバーのオリジンがhttp://localhost:5173でバックエンドの開発用サーバーのオリジンがhttp://localhost:3000である場合には、これらは異なるオリジンです。そのため、Fetch APIを使用してフロントエンドのアプリケーションからバックエンドのアプリケーションのリソースへアクセスすることはブラウザによって制限されます。
CORS(Cross-Origin Resource Sharing)は、このような場合でも異なるオリジンのリソースへアクセスすることを可能にする仕組みです。クライアントからのリクエストに対して、サーバーがHTTPレスポンスヘッダにリソースへのアクセスを許可するオリジンを示すAccess-Control-Allow-Originヘッダを含めることで、ブラウザはそこで指定されたオリジンからアクセスすることを許可します。
Expressを用いる場合には、corsパッケージを使用することで、HTTPレスポンスヘッダにAccess-Control-Allow-Originヘッダなどを適切に設定することができます。
-
次のコマンドを実行して、
corsパッケージとその型定義である@types/corsパッケージをインストールします。$ npm install cors
$ npm install -D @types/cors -
.envファイルを編集し、環境変数WEB_ORIGINの値にViteの開発用サーバーのオリジンであるhttp://localhost:5173を指定します。 -
main.tsファイルを編集し、CORSに関する設定を行います。14行目のapp.use(cors({ origin: process.env.WEB_ORIGIN }));により、環境変数WEB_ORIGINに指定したオリジンからアクセスすることができるようにします。backend/main.tsimport express from "express";
import cors from "cors";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client.js";
const app = express();
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
const client = new PrismaClient({ adapter });
app.use(express.json());
app.use(cors({ origin: process.env.WEB_ORIGIN }));
app.get("/posts", async (request, response) => {
const posts = await client.post.findMany();
response.json(posts);
});
app.post("/posts", async (request, response) => {
await client.post.create({ data: { message: request.body.message } });
response.sendStatus(201); // Created(新しいメッセージを作成)
});
app.listen(3000);
開発によく使うコマンドを登録する
package.jsonのscriptsプロパティに開発によく使うコマンドを登録します。次のように記載して、npm run devコマンドを実行することで開発用サーバーを起動できるようにしましょう。tsxパッケージでは、Node.jsと同様のオプションが利用できます。
{
"scripts": {
"dev": "tsx --env-file=.env main.ts"
}
}
動作を確認する
npm run devコマンドを実行して開発用サーバーを起動し、http://localhost:3000/postsにアクセスして、掲示板の投稿の一覧が取得できることを確認します。
フロントエンドを構築する
次の手順に従って、フロントエンドを構築しましょう。
Reactのセットアップをする
カレントディレクトリをプロジェクトを格納するディレクトリに移動してから、npm create vite@latestコマンドを実行してfrontendという名前でReactのプロジェクトを作成します。カレントディレクトリをfrontendディレクトリに移動してから、npm installコマンドを実行して必要なパッケージをインストールします。
バックエンドのURLを環境変数で指定する
.envファイルを作成し、環境変数VITE_API_ENDPOINTの値にバックエンドのURLであるhttp://localhost:3000を指定します。
アプリケーションのコードを記述する
App.tsxファイルを編集し、次のように記述します。掲示板の投稿の取得や、新しい投稿の送信には、fetch関数を使用します。fetch関数の第1引数は今までは/postsのように指定していましたが、ここでは${import.meta.env.VITE_API_ENDPOINT}/postsと指定します。import.meta.env.VITE_API_ENDPOINTで環境変数VITE_API_ENDPOINTに指定されたバックエンドのURLを利用しています。
import { useEffect, useState } from "react";
type Post = { id: number; message: string };
function App() {
const [posts, setPosts] = useState<Post[]>([]);
const [newPostContent, setNewPostContent] = useState("");
useEffect(() => {
async function fetchPosts() {
const response = await fetch(
`${import.meta.env.VITE_API_ENDPOINT}/posts`,
);
setPosts(await response.json());
}
fetchPosts();
}, []);
return (
<>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.message}</li>
))}
</ul>
<input
value={newPostContent}
onChange={(e) => {
setNewPostContent(e.target.value);
}}
/>
<button
type="button"
onClick={async () => {
await fetch(`${import.meta.env.VITE_API_ENDPOINT}/posts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: newPostContent }),
});
}}
>
送信
</button>
</>
);
}
export default App;
Viteは起動時に.envファイルの内容を読み込みます。
Viteは、VITE_で始まる環境変数を、アプリケーション内でimport.meta.envオブジェクトのプロパティとして利用できるようにします。例えば、VITE_API_ENDPOINTという環境変数の値は、import.meta.env.VITE_API_ENDPOINTと記述することで利用できます。
動作を確認する
npm run devコマンドを実行してViteの開発用サーバーを起動し、http://localhost:5173/にアクセスして、正しく動作することを確認します。
デプロイする
今までの開発環境では、バックエンドとフロントエンドのそれぞれでnpm run devコマンドを実行することで、バックエンドの場合にはtsxパッケージでTypeScriptファイルを実行して開発用サーバーを起動し、フロントエンドの場合にはViteの開発用サーバーを起動していました。
しかしながら、本番環境では、これとは異なる方法を用いることが一般的です。バックエンドの場合には、TypeScriptファイルをJavaScriptファイルにトランスパイルしてから、そのJavaScriptファイルを実行してWebサーバーを起動します。フロントエンドの場合には、Viteにより出力されたファイルをRenderで配信します。
| 環境 | バックエンド | フロントエンド |
|---|---|---|
| 開発環境 | tsxパッケージでTypeScriptファイルを実行して開発用サーバーを起動 | Viteの開発用サーバーを起動 |
| 本番環境 | TypeScriptファイルをJavaScriptファイルにトランスパイルしてから、JavaScriptファイルを実行してWebサーバーを起動 | Viteにより出力されたファイルをRenderで配信 |
ビルドの設定をする
フロントエンドの場合には、npm run buildコマンドを実行することで、Viteによるビルド結果がdistディレクトリに格納されるのでした。本番環境では、このdistディレクトリをRenderで配信すればよいです。
次に、バックエンドのTypeScriptファイルをトランスパイルするための設定を行いましょう。
-
tsconfig.jsonのoutDirオプションの値を"./dist"にして、トランスパイル結果がdistディレクトリに入るようにします。また、.gitignoreファイルに/distを追加して、distディレクトリがGitの管理下に入らないようにします。 -
package.jsonのscriptsプロパティに次のように記載して、npm run buildコマンドとnpm startコマンドが使えるようにしましょう。npm run buildコマンドでTypeScriptファイルをJavaScriptファイルにトランスパイルし、npm startコマンドで出力されたJavaScriptファイルを実行できるようになります。backend/package.jsonの抜粋{
"scripts": {
"dev": "tsx --env-file=.env main.ts",
"build": "prisma generate && tsc",
"start": "node dist/main.js"
}
}
Renderにデプロイする
次の手順に従って、作成したアプリケーションをRenderにデプロイしましょう。Renderにデプロイするには、作成したアプリケーションをGitHubのリポジトリに保存しておく必要があります。
-
バックエンドをデプロイするため、Renderにログインした直後の画面から
Web Serviceを作成します。
-
デプロイするアプリケーションのコードを含むGitHubのリポジトリを選択した後、次のように設定します。

-
環境変数
DATABASE_URLでデータベースへの接続情報を設定します。環境変数WEB_ORIGINは、ひとまず空のままにしておきます。
-
フロントエンドをデプロイするため、
Static Siteを作成します。
-
次のように設定します。

-
環境変数
VITE_API_ENDPOINTに先ほどデプロイしたバックエンドのURLを設定します。
-
バックエンドの環境変数の設定を再度開き、環境変数
WEB_ORIGINに先ほどデプロイしたフロントエンドのオリジンを設定します。