PR

ViteとReactのアプリをGo言語(Golang)のサーバーから配信する方法

応用

こんにちは。Tomoyuki(@tomoyuki65)です。

Go言語(Golang)には様々なフレームワークがありますが、現時点ではまだまともなフルスタックフレームワークはないため、Go言語を中心とした管理画面を作ったりしている方は少ないと思います。

そこでフロントエンドで人気のReact.jsを使い、ReactのアプリをGo言語のサーバーから配信することはできるのかが気になったので試してみることにしました。

React.jsを使う際は、Next.jsを除けば最近だとViteで開発することが多いと思うので、フロントエンドにはViteのReactを使い、バックエンドはGoとechoを使います。

この記事では、そんなViteとReactのアプリをGo言語(Golang)のサーバーから配信する方法についてご紹介します。

 

ViteとReactのアプリをGo言語(Golang)のサーバーから配信する方法

まずはViteのReactでアプリをさくっと作りますが、今回はDockerを使って開発環境を構築するため、必要に応じて事前にDocker Desktopなどを利用できるようにして下さい。

では以下のコマンドを実行し、各種ファイル等を作ります。

$ mkdir go-react && cd go-react
$ mkdir -p deploy/docker/local && touch deploy/docker/local/frontend.Dockerfile
$ touch compose.yml
$ mkdir frontend

 

次に作成したファイルをそれぞれ以下のように記述します。

・「deploy/docker/local/frontend.Dockerfile」

FROM node:22.19.0-alpine

WORKDIR /app

EXPOSE 5173

※今回はNodeのバージョン「22.19.0」を使います。

 

・「compose.yml」

services:
  frontend:
    container_name: frontend
    build:
      context: .
      dockerfile: ./deploy/docker/local/frontend.Dockerfile
    command: sh -c "npm ci && npm run dev -- --host"
    volumes:
      - ./frontend:/app
    ports:
      - "5173:5173"
    tty: true
    stdin_open: true

 

次に以下のコマンドを実行し、コンテナをビルドします。

$ docker compose build --no-cache

 

次に以下のコマンドを実行し、Viteを使ってReactのプロジェクトを作ります。

$ cd frontend
$ docker compose run --rm frontend npm create vite@latest ./frontend

 

コマンド実行後に選択肢が出るので、Ok to proceed? (y)には「y」、Select a frameworkには「React」、Select a variantには「TypeScript + SWC」を選択して実行します。

※SWCはRust製の高速なコンパイラです。

 

実行後、frontendディレクトリ配下に各種ファイルが作成されればOKです。

 

次に以下のコマンドを実行し、依存関係をインストールします。

$ cd ..
$ docker compose run --rm frontend npm i

 

次に以下のコマンドを実行し、コンテナを起動します。

$ docker compose up -d

 

次にブラウザで「http://localhost:5173」を開き、以下のような画面が表示されればOKです。

 

これでフロントエンド側のアプリは用意できたので、以下のコマンドを実行して一度コンテナを止めます。

$ docker compose down

 

スポンサーリンク

Goのechoでサーバー構築

次にGoのechoでサーバーを構築するため、以下のコマンドを実行して各種ファイルを作成します。

$ touch deploy/docker/local/backend.Dockerfile
$ mkdir backend && touch backend/main.go

 

次に作成したファイルをそれぞれ以下のように記述します。

・「deploy/docker/local/backend.Dockerfile」

##############################
# ビルドステージ
##############################
FROM node:22.19.0-alpine AS builder

WORKDIR /build

COPY ./frontend .

RUN npm ci

RUN npm run build

##############################
# 実行ステージ
##############################
FROM golang:1.25.1-alpine3.22 AS runner

WORKDIR /app

COPY ./backend .
COPY --from=builder /build/dist ./static

# go.modがあれば依存関係をインストール
RUN if [ -f ./go.mod ]; then \
      go install; \
    fi

# 開発用のライブラリをインストール
RUN go install github.com/air-verse/air@v1.63.0
RUN go install honnef.co/go/tools/cmd/staticcheck@latest
RUN go install go.uber.org/mock/mockgen@latest
RUN go install github.com/swaggo/swag/cmd/swag@latest

EXPOSE 8080

※Goのバージョンは「1.25.1」を使います。ホットリロード用のairのバージョンも「v1.63.0」で固定します。

 

・「backend/main.go」

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World !!")
}

 

次にファイル「compose.yml」を以下のように修正します。

services:
  frontend:
    container_name: frontend
    build:
      context: .
      dockerfile: ./deploy/docker/local/frontend.Dockerfile
    command: sh -c "npm ci && npm run dev -- --host"
    volumes:
      - ./frontend:/app
    ports:
      - "5173:5173"
    tty: true
    stdin_open: true
    depends_on:
      - backend
  backend:
    container_name: backend
    build:
      context: .
      dockerfile: ./deploy/docker/local/backend.Dockerfile
    command: air -c .air.toml
    volumes:
      - ./backend:/app
      - /app/static
    ports:
      - "8080:8080"
    tty: true
    stdin_open: true

 

次に以下のコマンドを実行し、コンテナをビルドします。

$ docker compose build --no-cache

 

次に以下のコマンドを実行し、初期化します。

$ docker compose run --rm backend go mod init go-react
$ docker compose run --rm backend air init

 

次にDDD(ドメイン駆動設計)を参考に、フロントエンドのアプリを配信するためのエンドポイントを作るため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir -p backend/internal/presentation/handler/admin
$ touch backend/internal/presentation/handler/admin/admin_handler.go
$ mkdir -p backend/internal/registry
$ touch backend/internal/registry/registry.go
$ mkdir -p backend/internal/presentation/router
$ touch backend/internal/presentation/router/router.go

 

次に作成したファイルをそれぞれ以下のように記述します。

・「backend/internal/presentation/handler/admin/admin_handler.go」

package admin

import (
    "io/fs"
    "net/http"

    "github.com/labstack/echo/v4"
)

type AdminHandler interface {
    Frontend(c echo.Context) error
}

type adminHandler struct {
   distFS fs.FS
}

func NewAdminHandler(distFS fs.FS) AdminHandler {
    return &adminHandler{
       distFS fs.FS
    }
}

func (h *adminHandler) Frontend(c echo.Context) error {
    // index.htmlを読み込む
    indexHTML, err := fs.ReadF ile(h.distFS, "index.html")
    if err != nil {
        return err
    }

    return c.HTML(http.StatusOK, string(indexHTML))
}

※セキュリティの問題で保存できなかったので「fs.ReadF ile」のFとiの間にスペースを入れています。実際のコードではスペースを消去して下さい。

 

・「backend/internal/registry/registry.go」

package registry

import (
    "io/fs"

    handlerAdmin "go-react/internal/presentation/handler/admin"
)

// ハンドラーをまとめるコントローラー構造体
type Controller struct {
    Admin handlerAdmin.AdminHandler
}

func NewController(distFS fs.FS) *Controller {
    // adminドメインのハンドラー設定
    adminHandler := handlerAdmin.NewAdminHandler(distFS)

    return &Controller{
        Admin: adminHandler,
    }
}

 

・「backend/internal/presentation/router/router.go」

package router

import (
    "io/fs"
    "net/http"

    "go-react/internal/registry"

    "github.com/labstack/echo/v4"
    echoMiddleware "github.com/labstack/echo/v4/middleware"
)

func SetupRouter(c *registry.Controller, distFS fs.FS) *echo.Echo {
    r := echo.New()

    // 共通ミドルウェア設定
    r.Use(echoMiddleware.Recover())

    // ルートパス"/"へのアクセスは404を返し、後続のStaticFSでindex.html を配信するのを防ぐ
    r.GET("/", func(c echo.Context) error {
        return echo.NewHTTPError(http.StatusNotFound, "Not Found")
    })

    // Adminのルーティング設定
    admin := r.Group("/admin")
    admin.GET("", c.Admin.Frontend)
    admin.GET("/*", c.Admin.Frontend)

    // 静的ファイルを配信
    r.StaticFS("/", distFS)

    return r
}

 

次に「backend/main.go」を以下のように修正します。

package main

import (
    "embed"
    "fmt"
    "io/fs"
    "log/slog"

    "go-react/internal/presentation/router"
    "go-react/internal/registry"
)

// ファイルの埋め込み
//go:embed static/*
var staticFiles embed.FS

func main() {
    // 埋め込んだファイルから"static"ディレクトリを取り出す
    distFS, err := fs.Sub(staticFiles, "static")
    if err != nil {
        slog.Error("埋め込んだファイルからstaticディレクトリの取り出しに失敗しました。")
    }

    // サーバー起動
    startPort := fmt.Sprintf(":%s", "8080")
    c := registry.NewController(distFS)
    r := router.SetupRouter(c, distFS)
    r.Logger.Fatal(r.Start(startPort))
}

 

次に以下のコマンドを実行し、go.modを更新します。
$ docker compose run --rm backend go mod tidy

 

次に以下のコマンドを実行し、コンテナを再ビルドします。

$ docker compose down
$ docker compose build --no-cache

 

次に以下のコマンドを実行し、コンテナを起動します。

$ docker compose up -d

 

次にブラウザで「http://localhost:8080/admin」を開き、以下のような画面が表示されればOKです。

 

スポンサーリンク

本番環境用のDockerコンテナを作る

上記ではdocker-composeを使ってフロントエンドとバックエンドでそれぞれコンテナを立てて開発しましたが、実際に本番環境にデプロイする際には一つのDockerfileでコンテナを構築する必要があります。

そこで本番環境用のDockerコンテナを作って試すため、以下のコマンドを実行してファイルを作成します。

$ mkdir -p deploy/docker/prod && touch deploy/docker/prod/Dockerfile

 

次に作成したファイルを以下のように記述します。

・「deploy/docker/prod/Dockerfile」

##############################
# ビルドステージ(node)
##############################
FROM node:22.19.0-alpine AS builder-node

WORKDIR /build

COPY ./frontend .

RUN npm ci

RUN npm run build

##############################
# ビルドステージ(go)
##############################
FROM golang:1.25.1-alpine3.22 AS builder-go

WORKDIR /build

COPY ./backend .
COPY --from=builder-node /build/dist ./static

RUN go install

RUN go build -o main .

##############################
# 実行ステージ
##############################
FROM alpine:3.22 AS runner

WORKDIR /app

# コンテナ用ユーザー作成
RUN addgroup --system --gid 1001 appuser && \
    adduser --system --uid 1001 appuser

COPY --from=builder-go /build/main .

EXPOSE 8080

USER appuser

CMD ["./main"]

 

次に以下のコマンドを実行し、コンテナをビルドおよび起動します。

$ docker compose down
$ docker build --no-cache -f deploy/docker/prod/Dockerfile -t go-react-app:latest .
$ docker run -d -p 80:8080 go-react-app:latest

 

次にブラウザで「http://localhost/admin」を開き、以下のような画面が表示されればOKです。

 

開発時にReactのアプリからGoのAPIを実行させる方法について

上記ではGoで構築したサーバーからReactのアプリを配信できることまで確認できましたが、実際にはReactからGoで構築したAPIを実行させる必要も出てくると思います。

本番環境にデプロイする際は同一ドメインのため問題ありませんが、開発時はフロントエンドとバックエンドでポート番号が異なる(ドメインが異なる)ため、それに対応するためにViteのコンフィグでGoのAPIへのプロキシ設定が必要になります。

具体的な設定方法としては、「frontend/vite.config.ts」を以下のように修正して下さい。

・・・

export default defineConfig({
  plugins: [react(),

  // goのapiへのプロキシ設定を追加する
  server: {
    proxy: {
      // '/api' という文字列を含むリクエストをプロキシする
      '/api': {
        // 転送先については、compose.ymlのコンテナ名を指定
        target: 'http://backend:8080',
        // オリジンを書き換える
        changeOrigin: true
      }
    }
  },

・・・

 

スポンサーリンク

最後に

今回はViteとReactのアプリをGo言語(Golang)のサーバーから配信する方法について解説しました。

RDB用の管理画面を作る際は、RailsやLaravelでさくっと作ることが多かったりすると思うので、Go言語で作っているケースは少ないと思いますが、今回のようにReactを組み合わせて作ることも可能です。

Go言語のサーバーからReactなどのフロントエンドを配信したい場合は、ぜひ参考にしてみて下さい。

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はWeb系エンジニアをしています!

Tomoyukiをフォローする
応用
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました