1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoLangとReactとConnectRPCで型安全な開発環境を作る - devcontainer版

Last updated at Posted at 2025-07-05

はじめに

現代の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

参考にした情報は以下のとおりです。

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+PTasks: Run TaskStart Backend Dev: 通常モードで実行
  • Ctrl+Shift+PTasks: Run TaskStart 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 BackendDebug Frontend でそれぞれ単独デバッグ可能
  • フルスタックデバッグ: Debug Full Stack でフロントエンドとバックエンドを同時にデバッグ実行
  • 統合停止: stopAll: true により、一方を停止すると両方が停止

デバッグ実行の手順

  1. VS Codeのデバッグビューを開く(Ctrl+Shift+D)
  2. 「Debug Full Stack」を選択
  3. 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であってもこの基本指針があることで、血迷ったことをするケースが減るように感じます。

本記事の内容が少しでも快適な開発環境につながれば幸いです。
ここまでお読みいただきありがとうございました。

追記:本記事でできたリポジトリを以下に公開しました

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?