はじめに
現代のWebアプリケーション開発では、フロントエンドとバックエンドの間で型安全性を保ちながら効率的に開発することが重要です。本記事では、シンプルなGreeterサービスを例に、以下の技術スタックを使用して型安全なフルスタックアプリケーションを構築する方法を解説します。
今回の例では、個人開発などのスモールな開発から、負荷分散などが必要な大規模な開発まで対応可能な、シンプルで拡張性のあるアーキテクチャを目指します。
すでに多くの先人が本アーキテクチャでの紹介記事を記載していますが、DevContainerを用いて開発環境をゼロから構築する点について、特に意識して記載しています。
本記事の例は、あくまで開発の学習を目的としています。実開発やユーザ向けサービスの作成の際には、過信せず適切な設計を行ってください。
本記事の内容を用いた結果のあらゆる事象は、著者の責任範囲外とします。
使用技術
- Backend: Go + Connect-RPC
- Frontend: React + TypeScript + Material-UI
- 開発環境: VS Code Dev Containers
- スキーマ管理: Protocol Buffers + Buf
- デプロイ: Docker + nginx
システム構成図
┌─────────────────────────────────────────────────────────────────┐
│ Dev Container Environment │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────┐│
│ │ Frontend │ │ Backend │ │ Protocol ││
│ │ React + │◄──►│ Go + │◄──►│ Buffers ││
│ │ TypeScript │ │ Connect-RPC │ │ Schema ││
│ │ │ │ │ │ ││
│ │ Port: 5173 │ │ Port: 18080 │ │ .proto files ││
│ └─────────────────┘ └─────────────────┘ └───────────────┘│
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Production │
│ nginx + │
│ Docker │
│ Port: 8080 │
└─────────────────┘
本アーキテクチャの解説
Connect-RPCとは
Connect-RPCは、HTTP/JSON上で動作するgRPCライクなRPCフレームワークです。従来のgRPCと比較して以下の利点があります:
- HTTP/1.1とHTTP/2の両方をサポート
- ブラウザから直接アクセス可能
- JSONとバイナリプロトコルの両方をサポート
- 型安全性を保ちながらシンプルな実装
特にgRPC互換を保ちつつもブラウザから直接アクセス可能(envoyなどが不要)な点は、利用のハードルが低く、gRPCとの相互運用も可能な便利なプロトコルとなっております。
Next.jsなどとの比較
Next.jsなど、フロントエンドとバックエンドを一気通貫で実装できるフレームワークが多くある今、このように言語スタックを分ける動機について、解説します。
Next.jsは人気ですが、非常に大きなフレームワークです。
人気な要素には、バックエンドもTypeScriptで書けて学習コストが低いこと、バックエンドとフロントエンドの間の接合の意識が薄くても済むこと、などがあるのではないかと考えています。
一方、TypeScriptはどうしてもJavaScriptに型を後付けして後方互換性を維持している観点から、サーバサイドを記述する際にもJavaScriptエンジン(≒ブラウザ)の制約を意識する必要があります。
この点に関して、バックエンドを記述する場合には本来不要なことを多く意識する必要もあると感じています。
また、バックエンドの言語スタックを分ける前提に立つと、チームの状況や対象とするドメインに応じて、適切な言語選定を行いやすくなるため、アーキテクチャのビジネススケール可能性の点で有利になるケースがあると考えています。
本記事ではバックエンドにはバックエンド特化でのGoLangを使うことで、シンプルなバックエンド実装を可能にする方法を模索しました。
メリット/デメリット
今回のアーキテクチャは、gRPCベースのアプローチを取っています。REST(OpenAPI)やGraphQLなどと比べ、以下のメリットとデメリットがあると考えています。
- メリット:コード生成エコシステムが充実しており、開発者体験が良い。
- デメリット:サードパーティーからのAPI利用にハードルがある。
特に以下のような場合は、上記特性以外も考慮の上、本アーキテクチャ以外の選択肢も視野に入れて検討したほうが良いと考えます。
- サードパーティーへのAPI公開が目的の中で多くの比重を占める開発の場合
- RESTおよびGraphQLの選択が適切な可能性があります。
- フロントエンドにリッチな実装が不要の場合
- Spring Framework や Django など、レガシーなフレームワークで組むほうが適切な場合があります。
- 開発チームがすでに別のフレームワークに習熟している場合
- 現在は様々なフルスタックフレームワークが出ています。すでにチームが好むフレームワークがある場合、優先的にそちらを採用する動機があります。
Connect-RPCの通信フロー
┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │
│ (React) │ │ (Go) │
├─────────────────┤ ├─────────────────┤
│ Connect Client │───── HTTP/JSON ───────►│ Connect Server │
│ │ │ │
│ TypeScript │◄─── Response ──────────│ Go Structs │
│ Generated Code │ │ Generated Code │
└─────────────────┘ └─────────────────┘
▲ ▲
│ │
└─────────── Protocol Buffers ──────────────┘
(.proto files → Code Generation)
用意する環境
必要なツール
- Docker Desktop
- VS Code
- VS Code Dev Containers拡張機能
この記事では、開発環境をすべてDev Container内に構築するため、ローカルマシンにGo、Node.jsをインストールする必要はありません。
devcontainerの準備
ディレクトリ構成の設定
まず、プロジェクトの基本構造を作成します:
greeter-app/
├── .devcontainer/
│ ├── devcontainer.json
│ ├── compose.yaml
│ └── app/
│ └── Dockerfile
├── backend/
├── frontend/
├── proto/
└── docker/
プロジェクト構成図
┌─────────────────────────────────────────────────────────────────┐
│ greeter-app/ │
├─────────────────────────────────────────────────────────────────┤
│ .devcontainer/ │
│ ├── devcontainer.json ◄─── Dev Container設定 │
│ ├── compose.yaml ◄─── Docker Compose設定 │
│ └── app/Dockerfile ◄─── 開発環境用Dockerfile │
│ │
│ backend/ ◄─── Go + Connect-RPC Server │
│ ├── main.go │
│ ├── go.mod │
│ └── gen/ ◄─── Protocol Buffers生成コード │
│ │
│ frontend/ ◄─── React + TypeScript Client │
│ ├── app/ │
│ │ ├── routes/ │
│ │ ├── components/ │
│ │ └── gen/ ◄─── Protocol Buffers生成コード │
│ └── package.json │
│ │
│ proto/ ◄─── Protocol Buffers Schema │
│ ├── buf.yaml │
│ ├── buf.gen.yaml │
│ └── greeter/v1/greeter.proto │
│ │
│ docker/ ◄─── 本番環境用設定 │
│ ├── nginx.conf │
│ └── start.sh │
│ │
│ Dockerfile ◄─── 本番環境用マルチステージビルド │
│ compose.yaml ◄─── Docker環境実行用 │
│ .dockerignore ◄─── Docker用 │
└─────────────────────────────────────────────────────────────────┘
devcontainer設定ファイル
.devcontainer/devcontainer.json
を作成:
{
"name": "Go, TypeScript, connectrpc",
"dockerComposeFile": "compose.yaml",
"workspaceFolder": "/workspace",
"service": "app",
"remoteUser": "devcontainer",
"updateRemoteUserUID": true,
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"EditorConfig.EditorConfig",
"vitest.explorer"
]
}
}
}
Docker Compose設定
.devcontainer/compose.yaml
を作成:
services:
app:
build:
context: app
dockerfile: Dockerfile
volumes:
- ..:/workspace
working_dir: /workspace
init: true
command: sleep infinity
開発コンテナのDockerfile
.devcontainer/app/Dockerfile
を作成:
FROM debian:bookworm
# Install golang
ARG GO_VERSION=1.24.4
RUN apt-get update && \
apt-get install -y curl gzip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /temp && \
cd /temp && \
curl -O -L https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz && \
rm -rf /temp/go${GO_VERSION}.linux-amd64.tar.gz
ENV PATH=$PATH:/usr/local/go/bin
# Install nodejs
ARG NVM_VERSION=0.40.3
ARG NODE_VERSION=24.2.0
ENV NVM_DIR=/usr/local/nvm
WORKDIR $NVM_DIR
RUN mkdir -p $NVM_DIR && \
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash && \
. $NVM_DIR/nvm.sh && \
nvm install ${NODE_VERSION} && \
nvm alias default ${NODE_VERSION} && \
nvm use default
ENV PATH=$PATH:$NVM_DIR/versions/node/v${NODE_VERSION}/bin
# Install additional tools
RUN apt-get update && \
apt-get install -y \
git \
procps \
lsof \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create user and group for development
ARG DEV_USER=devcontainer
ARG DEV_GROUP=devcontainer
ARG DEV_UID=1000
ARG DEV_GID=1000
RUN groupadd -g $DEV_GID $DEV_GROUP && \
useradd -m -s /bin/bash -u $DEV_UID -g $DEV_GID $DEV_USER
USER $DEV_USER
# Install devtools
RUN go install github.com/go-delve/delve/cmd/dlv@latest && \
go install github.com/bufbuild/buf/cmd/buf@latest && \
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest && \
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
ENV PATH=$PATH:/home/$DEV_USER/go/bin
RUN npm install -g \
@bufbuild/buf \
@bufbuild/protoc-gen-connect-query \
@bufbuild/protoc-gen-es
参考にした情報は以下のとおりです。
- Download and install - The Go Programming Language - Go言語のインストール方法
- Node.js — Node.js®をダウンロードする - Node.jsのインストール情報(Officialに推奨されているnvmを利用)
Protocol Buffersの作成
Bufの設定
型安全性の核心となるProtocol Buffersスキーマを定義します。
proto/buf.yaml
を作成:
version: v2
lint:
use:
- STANDARD
breaking:
use:
- FILE
modules:
- path: .
コード生成設定
proto/buf.gen.yaml
を作成:
version: v2
plugins:
- local: protoc-gen-go
out: ../backend/gen
opt: paths=source_relative
- local: protoc-gen-connect-go
out: ../backend/gen
opt: paths=source_relative
- local: protoc-gen-es
out: ../frontend/app/gen
include_imports: true
opt: target=ts
- local: protoc-gen-connect-query
out: ../frontend/app/gen
opt: target=ts
Greeterサービスの定義
proto/greeter/v1/greeter.proto
を作成:
syntax = "proto3";
package greeter.v1;
option go_package = "backend/gen/greeter/v1;greeterv1";
// Request message for greeting
message GreetRequest {
string name = 1;
}
// Response message for greeting
message GreetResponse {
string message = 1;
}
// Service definition
service GreeterService {
rpc Greet(GreetRequest) returns (GreetResponse);
}
バックエンド, フロントエンドのベースを作成
バックエンド
Go modulesの初期化
cd backend
go mod init backend
必要な依存関係をインストール
go get connectrpc.com/connect
go get github.com/rs/cors
go get google.golang.org/protobuf
バックエンド構成
backend/
├── main.go # エントリーポイント
├── go.mod
└── go.sum
フロントエンド
React Router v7プロジェクトの初期化
cd frontend
npx create-react-router@latest .
SPAモードを有効にする
react-router v7では、デフォルトでサーバーサイドレンダリング(SSR)が有効になっています。
今回はSPAモードに切り替えるために、以下の設定を行います。
react-router.config.tsを以下のように変更:
import type { Config } from "@react-router/dev/config";
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: false,
} satisfies Config;
必要な依存関係をインストール
npm install @connectrpc/connect @connectrpc/connect-web
npm install @bufbuild/protobuf
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material
フロントエンド構成
frontend/
├── package.json
├── vite.config.ts
├── tsconfig.json
├── react-router.config.ts # React Router v7設定
├── app/
│ ├── root.tsx
│ ├── routes.ts
│ ├── routes/
│ │ └── home.tsx
│ ├── services/
│ │ └── greeter.ts
│ └── components/
│ └── GreeterForm.tsx
└── public/
└── favicon.ico
Protocol Buffersからのコード生成
cd proto
buf generate
このコマンドにより、GoとTypeScriptの両方で型安全なクライアント・サーバーコードが生成されます。
コード生成フロー
┌─────────────────┐ ┌──────────────────┐
│ .proto │ │ Generated │
│ Schema │ │ Code │
├─────────────────┤ buf generate ├──────────────────┤
│ greeter.proto │─────────────────────► │ │
│ │ │ Backend (Go) │
│ message │ │ ├─greeter.pb.go │
│ GreetRequest │ │ └─greeter. │
│ │ │ connect.go │
│ service │ │ │
│ GreeterService │ │ Frontend (TS) │
│ │ │ ├─greeter_pb.ts │
│ │ │ └─greeter- │
│ │ │ GreeterService│
│ │ │ _connectquery.│
│ │ │ ts │
└─────────────────┘ └──────────────────┘
注意事項:
- 生成されるTypeScriptファイルは
@bufbuild/connect-query
ベースになります - ファイル名は
greeter-GreeterService_connectquery.ts
のような形式になります - 型定義は
greeter_pb.ts
に生成されます
バックエンドの実装
実装
Connect-RPCサーバーの実装
backend/main.go
:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"connectrpc.com/connect"
"github.com/rs/cors"
greeterv1 "backend/gen/greeter/v1"
"backend/gen/greeter/v1/greeterv1connect"
)
type GreeterServer struct{}
func (s *GreeterServer) Greet(
ctx context.Context,
req *connect.Request[greeterv1.GreetRequest],
) (*connect.Response[greeterv1.GreetResponse], error) {
name := req.Msg.Name
if name == "" {
name = "World"
}
message := fmt.Sprintf("Hello, %s!", name)
return connect.NewResponse(&greeterv1.GreetResponse{
Message: message,
}), nil
}
func main() {
greeterServer := &GreeterServer{}
mux := http.NewServeMux()
path, handler := greeterv1connect.NewGreeterServiceHandler(greeterServer)
// APIパスをプレフィックス付きで設定
mux.Handle("/api"+path, handler)
// 開発環境互換用(直接アクセス)
mux.Handle(path, handler)
// 環境変数から設定を取得
debug := os.Getenv("DEBUG") == "true"
allowedOrigins := os.Getenv("ALLOWED_ORIGINS")
if debug {
// デバッグモードの場合は全てのオリジンを許可
allowedOrigins = "*"
fmt.Println("DEBUG mode enabled - allowing all origins")
}
// CORS設定
corsOptions := cors.Options{
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"*"},
AllowCredentials: true,
}
if debug {
corsOptions.AllowedOrigins = []string{"*"}
corsOptions.AllowCredentials = false // "*"を使用する場合はCredentialsをfalseにする必要がある
} else {
corsOptions.AllowedOrigins = strings.Split(allowedOrigins, ",")
}
c := cors.New(corsOptions)
port := os.Getenv("PORT")
if port == "" {
port = "18080"
}
fmt.Printf("Server starting on :%s\n", port)
fmt.Printf("Allowed origins: %s\n", allowedOrigins)
log.Fatal(http.ListenAndServe(":"+port, c.Handler(mux)))
}
ビルド、デバッグ
VS Code Tasksの設定
.vscode/tasks.json
を作成:
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Backend",
"type": "shell",
"command": "cd backend && go build -o backend-app .",
"group": "build"
},
{
"label": "Test Backend",
"type": "shell",
"command": "cd backend && go test ./...",
"group": "test"
},
{
"label": "Install Frontend Dependencies",
"type": "shell",
"command": "cd frontend && npm install",
"group": "build"
},
{
"label": "Build Frontend",
"type": "shell",
"command": "cd frontend && npm run build",
"group": "build",
"dependsOn": "Install Frontend Dependencies"
},
{
"label": "Generate Proto",
"type": "shell",
"command": "cd proto && buf generate",
"group": "build"
},
{
"label": "Start Backend Dev",
"type": "shell",
"command": "cd backend && go run .",
"group": "build",
"isBackground": true
},
{
"label": "Start Backend Debug",
"type": "shell",
"command": "cd backend && DEBUG=true go run .",
"group": "build",
"isBackground": true
}
]
}
開発用実行
# 通常の実行
cd backend
go run .
# デバッグモードで実行(全てのオリジンを許可)
cd backend
DEBUG=true go run .
VS Codeでの実行
- Ctrl+Shift+P → Tasks: Run Task → Start Backend Dev: 通常モードで実行
- Ctrl+Shift+P → Tasks: Run Task → Start Backend Debug: デバッグモードで実行
テストの実行
cd backend
go test ./...
フロントエンドの実装
実装
Connect-RPCクライアントの設定
frontend/app/services/greeter.ts
:
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { GreeterService } from "../gen/greeter/v1/greeter_pb";
// 環境変数からバックエンドURLを取得
const getBackendUrl = () => {
const backendUrl = import.meta.env.VITE_BACKEND_URL;
// 環境変数が設定されていない場合は相対パス(本番環境用)
if (!backendUrl) {
return "/api";
}
return backendUrl;
};
// トランスポートの作成
const transport = createConnectTransport({
baseUrl: getBackendUrl(),
});
export const greeterClient = createClient(GreeterService, transport);
Greeterコンポーネント
frontend/app/components/GreeterForm.tsx
:
import React, { useState } from 'react';
import {
Box,
TextField,
Button,
Typography,
Paper,
Alert
} from '@mui/material';
import { greeterClient } from '../services/greeter';
import { create } from '@bufbuild/protobuf';
import { GreetRequestSchema } from '../gen/greeter/v1/greeter_pb';
export const GreeterForm: React.FC = () => {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const request = create(GreetRequestSchema, { name });
const response = await greeterClient.greet(request);
setMessage(response.message);
} catch (err) {
setError('Failed to get greeting');
console.error('Greeting error:', err);
} finally {
setLoading(false);
}
};
return (
<Box sx={{ maxWidth: 500, mx: 'auto', mt: 4 }}>
<Paper elevation={3} sx={{ p: 4 }}>
<Typography variant="h4" gutterBottom align="center">
Greeter Service
</Typography>
<form onSubmit={handleSubmit}>
<TextField
fullWidth
label="Your Name"
value={name}
onChange={(e) => setName(e.target.value)}
margin="normal"
placeholder="Enter your name"
/>
<Button
type="submit"
variant="contained"
fullWidth
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Greeting...' : 'Get Greeting'}
</Button>
</form>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{message && (
<Box sx={{ mt: 3 }}>
<Typography variant="h6" align="center" color="primary">
{message}
</Typography>
</Box>
)}
</Paper>
</Box>
);
};
メインページの設定
frontend/app/routes/home.tsx
:
import React from 'react';
import { Container, CssBaseline, ThemeProvider, createTheme } from '@mui/material';
import { GreeterForm } from '../components/GreeterForm';
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
},
},
});
export function meta() {
return [
{ title: "Greeter Service" },
{ name: "description", content: "Connect-RPC Greeter Service" },
];
}
export default function Home() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container>
<GreeterForm />
</Container>
</ThemeProvider>
);
}
ビルド、デバッグ
開発環境の起動フロー
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Terminal 1 │ │ Terminal 2 │ │ Browser │
│ Backend │ │ Frontend │ │ Testing │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ cd backend │ │ cd frontend │ │ │
│ go run . │ │ npm run dev │ │ localhost:5173 │
│ │ │ │ │ │
│ Server: │ │ Dev Server: │ │ │
│ localhost:18080 │◄───┤ localhost:5173 │◄───┤ User Input │
│ │ │ │ │ │
│ Connect-RPC │ │ React Router │ │ ┌─────────┐ │
│ Handler │ │ + Vite │ │ │ Submit │ │
│ │ │ │ │ └─────────┘ │
│ CORS Enabled │ │ Proxy to │ │ │
│ │ │ Backend │ │ Response │
│ │ │ │ │ Display │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
HTTP Requests │
(CORS enabled) │
│
┌────────────────────────────┘
│
▼
┌─────────────────┐
│ Development │
│ Workflow │
│ │
│ 1. Edit Code │
│ 2. Auto Reload │
│ 3. Test Changes │
│ 4. Debug │
└─────────────────┘
VS Code Tasks設定の追加
上記で作成した .vscode/tasks.json
に、さらに開発用のタスクを追加します。
{
"version": "2.0.0",
"tasks": [
{
"label": "Build Backend",
"type": "shell",
"command": "cd backend && go build -o backend-app .",
"group": "build"
},
{
"label": "Test Backend",
"type": "shell",
"command": "cd backend && go test ./...",
"group": "test"
},
{
"label": "Install Frontend Dependencies",
"type": "shell",
"command": "cd frontend && npm install",
"group": "build"
},
{
"label": "Build Frontend",
"type": "shell",
"command": "cd frontend && npm run build",
"group": "build",
"dependsOn": "Install Frontend Dependencies"
},
{
"label": "Generate Proto",
"type": "shell",
"command": "cd proto && buf generate",
"group": "build"
},
{
"label": "Start Backend Dev",
"type": "shell",
"command": "cd backend && go run .",
"group": "build",
"isBackground": true
},
{
"label": "Start Backend Debug",
"type": "shell",
"command": "cd backend && DEBUG=true go run .",
"group": "build",
"isBackground": true
},
{
"label": "Start Frontend Dev",
"type": "shell",
"command": "cd frontend && npm run dev",
"group": "build",
"isBackground": true
}
]
}
統合デバッグ環境作成
launch.json
.vscode/launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Backend",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/backend",
"cwd": "${workspaceFolder}/backend",
"env": {
"DEBUG": "true",
"PORT": "18080"
}
},
{
"name": "Debug Frontend",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/frontend/node_modules/.bin/vite",
"args": ["dev", "--host", "0.0.0.0", "--port", "5173"],
"cwd": "${workspaceFolder}/frontend",
"env": {
"NODE_ENV": "development",
"VITE_BACKEND_URL": "http://localhost:18080/"
},
"console": "integratedTerminal"
}
],
"compounds": [
{
"name": "Debug Full Stack",
"configurations": [
"Debug Backend",
"Debug Frontend"
],
"stopAll": true
}
]
}
この設定により、以下の機能が利用可能になります:
-
個別デバッグ:
Debug Backend
、Debug Frontend
でそれぞれ単独デバッグ可能 -
フルスタックデバッグ:
Debug Full Stack
でフロントエンドとバックエンドを同時にデバッグ実行 -
統合停止:
stopAll: true
により、一方を停止すると両方が停止
デバッグ実行の手順
- VS Codeのデバッグビューを開く(Ctrl+Shift+D)
- 「Debug Full Stack」を選択
- F5キーまたは実行ボタンをクリック
これにより、バックエンドが http://localhost:18080
で、フロントエンドが http://localhost:5173
で同時に起動し、両方のブレークポイントが有効になります。
Dockerize(デプロイ準備)
Docker マルチステージビルド構成
┌─────────────────────────────────────────────────────────────────┐
│ Dockerfile (Multi-stage) │
├─────────────────────────────────────────────────────────────────┤
│ Stage 1: frontend-builder │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ FROM node:24.2.0-alpine ││
│ │ COPY frontend/ ││
│ │ RUN npm ci && npm run build ││
│ │ OUTPUT: /app/frontend/build/client/ ││
│ └─────────────────────────────────────────────────────────────┘│
│ │ │
│ Stage 2: backend-builder ▼ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ FROM golang:1.24.4-alpine ││
│ │ COPY backend/ ││
│ │ RUN go build -o backend-app ││
│ │ OUTPUT: /app/backend/backend-app ││
│ └─────────────────────────────────────────────────────────────┘│
│ │ │
│ Stage 3: production ▼ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ FROM nginx:alpine ││
│ │ COPY --from=frontend-builder /app/frontend/build/client ││
│ │ COPY --from=backend-builder /app/backend/backend-app ││
│ │ COPY docker/nginx.conf ││
│ │ COPY docker/start.sh ││
│ │ EXPOSE 8080 ││
│ │ CMD ["/app/start.sh"] ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
本番用Dockerfile
プロジェクトルートのDockerfile
:
# マルチステージビルド: Frontend Build stage
FROM node:24.2.0-alpine AS frontend-builder
# フロントエンドのビルド
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Backend Build stage
FROM golang:1.24.4-alpine AS backend-builder
# バックエンドのビルド
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o backend-app .
# Production stage
FROM nginx:alpine
# nginxの設定をコピー
COPY docker/nginx.conf /etc/nginx/nginx.conf
# ビルドされたバックエンドバイナリをコピー
COPY --from=backend-builder /app/backend/backend-app /app/backend/backend-app
RUN chmod +x /app/backend/backend-app
# ビルドされたフロントエンドの静的ファイルをコピー
COPY --from=frontend-builder /app/frontend/build/client /app/frontend/
# 起動スクリプトをコピー
COPY docker/start.sh /app/start.sh
RUN chmod +x /app/start.sh
# 作業ディレクトリを設定
WORKDIR /app
# nginxポート8080を公開
EXPOSE 8080
# 起動スクリプトを実行
CMD ["/app/start.sh"]
nginx設定
docker/nginx.conf
:
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream backend {
server 127.0.0.1:18080;
}
server {
listen 8080;
# Frontend static files
location / {
root /app/frontend;
try_files $uri $uri/ /index.html;
}
# Backend API(統合環境用)
location /api/ {
proxy_pass http://backend/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
}
起動スクリプト
docker/start.sh
:
#!/bin/sh
# Start backend in background
/app/backend/backend-app &
# Start nginx in foreground
nginx -g 'daemon off;'
Docker用の各種ファイル
compose.yaml
:
services:
app:
build: .
ports:
- "8080:8080"
.dockerignore
:
frontend/node_modules
ビルドとデプロイ
compose.yamlから呼ばれるマルチステージビルドを使用することで、ビルドプロセスが簡素化されます。
# Protocol Buffersからコード生成
cd proto
buf generate
# compose upする
docker compose up --build
デプロイフロー
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Development │ │ Build Stage │ │ Production │
│ Environment │ │ (Docker) │ │ Environment │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ Code Changes │────►│ buf generate │────►│ nginx:8080 │
│ │ │ │ │ │
│ - backend/ │ │ Frontend Build │ │ ┌─────────────┐ │
│ - frontend/ │ │ - npm ci │ │ │ Static │ │
│ - proto/ │ │ - npm run build │ │ │ Files │ │
│ │ │ │ │ └─────────────┘ │
│ VS Code │ │ Backend Build │ │ ┌─────────────┐ │
│ Dev Container │ │ - go build │ │ │ Backend │ │
│ │ │ │ │ │ API │ │
│ ┌─────────────┐ │ │ Final Image │ │ │ (Port 18080)│ │
│ │ Hot │ │ │ - nginx │ │ └─────────────┘ │
│ │ Reload │ │ │ - static files │ │ │
│ │ Enabled │ │ │ - backend app │ │ /api/* → Backend│
│ └─────────────┘ │ │ - start.sh │ │ /* → Frontend │
└─────────────────┘ └─────────────────┘ └─────────────────┘
おわりに
昨今、生成AIプログラミングなど、LLMの力を使ったエンジニアリングが話題になっています。
本記事執筆の動機の1つとして、この生成AIによるエンジニアリングとスキーマ駆動開発の相性が良さそうだったという点があります。
スキーマ ...今回の場合にはprotoファイル... をまず作り、それを満たすようなバックエンドとフロントエンドを書くという方法は、人間にとっても慣れると快適なものです。
生成AIであってもこの基本指針があることで、血迷ったことをするケースが減るように感じます。
本記事の内容が少しでも快適な開発環境につながれば幸いです。
ここまでお読みいただきありがとうございました。
追記:本記事でできたリポジトリを以下に公開しました