Web フロントエンドしかやってこなかった人がバックエンドに入門する

:::note warning
この記事は書きかけです。
:::

## 概要

　皆さん Web はお好きでしょうか。筆者はずっとフロントと esbuild プラグインばかりを書いてきてバックエンドはサボってきたのでそろそろバックエンドを触れるようになろうとおもいました。この記事はバックエンドに入門するために行ったことの記録です。

　今回は Docker Compose を用いてすべてのコンポーネントを作成します。すなわち nginx、API サーバー (Go)、データベース (PostgreSQL)、Redis をそれぞれ別のコンテナで作成しルーティングします。

　成果物のレポジトリは以下のものになります。

https://github.com/Tsukina-7mochi/web-backend-practice

## Step 1. 仮設 API サーバーを作る

### サーバーを書く

　今回は REST API サーバーを Go で書いていきます。まず `api` ディレクトリで `go mod init main` してプロジェクトを作り、`main.go` にサーバーのコードを記述していきます。一旦 `/ping` に対するリクエストに応答するだけのサーバーを作ります。簡単のためコマンドライン引数を扱わずに環境変数を使います。

 ```go:api/main.go
 package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func ping(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "pong")
}

func main() {
	addr := os.Getenv("ip")
	port := os.Getenv("port")

	if addr == "" {
		addr = "0.0.0.0"
	}
	if port == "" {
		port = "8080"
	}

	http.HandleFunc("/ping", ping)
	log.Printf("Server listening on %v:%v", addr, port)
	log.Fatal(http.ListenAndServe(addr+":"+port, nil))
}
 ```

　この状態で `go run main.go` をするとサーバーの待ち受けが開始します。問題がなければ `localhost:8080/ping` に GET することで pong と返ってくるはずです。

:::note
私の環境では docker compose でログを見るとき `fmt.Printf` を使うとフラッシュされずログが出力されませんでしたが、`log.Printf` を使うことで出力されるようになりました。
:::

### Docker, Docker Compose の設定
 
　次に開発環境の Dockerfile を書きます。練習なので root ユーザーでいいとします。 Go のインストールや PATH の設定を行っておきます。

 ```Dockerfile:api/Dockerfile
 FROM ubuntu:24.04 AS base

RUN apt update && apt upgrade -y


FROM base AS dev

ARG USERNAME

WORKDIR /tmp
RUN apt install -y wget tar
RUN wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
RUN tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
ENV PATH $PATH:/usr/local/go/bin

WORKDIR /api

CMD ["/bin/bash"]
 ```

　次にプロジェクトのルートディレクトリで `docker-compose.yml` を書きます。

 ```yaml:docker-compose.yml
services:
  api-dev:
    build:
      context: './api'
      dockerfile: './Dockerfile'
      target: dev
    volumes:
      - type: bind
        source: ./api
        target: /api
    working_dir: /api
    ports:
      - target: 8081
        published: 8081
    tty: true
    stdin_open: true
 ```

　これにより開発用のコンテナの設定ができました。今回はローカルにある `go` を使ってしまうのであまり使いませんが......。コンテナを立ち上げて `go run` すると待受が開始するはずです。今回はコンテナの 8080 ポートをホストの 8080 ポートに転送しているので先程と同様に `localhost:8080` でアクセスできるはずです。

```shell
$ docker compose up api-dev -d
$ docker compose exec api-dev /usr/local/go/bin/go run /api/main.go
$ curl localhost:8080/ping
# pong
```

### 本番環境を作る

　開発環境ができたので本番環境を作っていきます。Dockerfile に `build` ステージを追加してビルドを実行します。さらに `prod` ステージを追加して `build` ステージで作成したバイナリをコピーしてきます。これにより `build` ステージに開発環境は入らずバイナリだけが存在する状況にします。

 ```Dockerfile:api/Dockerfile
# 追加
FROM dev AS build

COPY . /api
RUN go clean
RUN go build -o /tmp/main /api/main.go


FROM base AS prod

COPY --from=build /api/main /api/server

CMD ["/api/server"]
```

　`docker-compose.yml` にもサービスを追加しておきます。

```yaml:docker-compose.yml
 # 追記
 services:
  api:
    build:
      context: './api'
      dockerfile: './Dockerfile'
      target: prod
    ports:
      - target: 8080
        published: 8080
```

　これで `docker compose up api` するだけでサーバーが待ち受けしてくれるようになりました。

### Air を導入する

https://github.com/air-verse/air

　Air は Go のライブリロードを提供するツールです。ソースファイルの変更が検出されると再ビルド・実行を自動で行ってくれます。API サーバーの編集を試行錯誤しているときに便利です。

```sh
$ go install air
$ air init
```

次に Dockerfile を編集します。

```Dockerfile
WORKDIR /tmp
RUN apt install -y wget tar
RUN wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
RUN tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
ENV GOPATH /root/go
ENV PATH $PATH:/usr/local/go/bin:$GOPATH

RUN go install github.com/air-verse/air@latest

WORKDIR /api
COPY ./go.mod ./go.sum ./
RUN go mod download

CMD ["/root/go/bin/air"]
```

## 2. nginx を設定する

### Docker Compose のコンテナ間ネットワークの動作確認

　コンテナ間ネットワークの知識がなかったので動作確認をしました。まず適当にクライアント用コンテナを用意します。

```Dockerfile:client/Dockerfile
FROM ubuntu:24.04

RUN apt update && apt upgrade -y
RUN apt install -y iputils-ping net-tools dnsutils curl wget
```

```yaml:docker-compose.yml
# 追記
services:
  client:
    build:
      context: './client'
      dockerfile: './Dockerfile'
    command: /bin/bash
    tty: true
    stdin_open: true
```

クライアントコンテナ内で色々実行してみます

 ```shell
$ ping api -c 3
PING api (172.23.0.3) 56(84) bytes of data.
64 bytes from web-backend-practice-api-1.web-backend-practice_default (172.23.0.3): icmp_seq=1 ttl=64 time=0.039 ms
64 bytes from web-backend-practice-api-1.web-backend-practice_default (172.23.0.3): icmp_seq=2 ttl=64 time=0.044 ms
64 bytes from web-backend-practice-api-1.web-backend-practice_default (172.23.0.3): icmp_seq=3 ttl=64 time=0.043 ms

--- api ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2047ms
rtt min/avg/max/mdev = 0.039/0.042/0.044/0.002 ms
```
```shell
$ nslookup api
Server:         127.0.0.11
Address:        127.0.0.11#53

Non-authoritative answer:
Name:   api
Address: 172.23.0.3
```
```shell
$ curl api:8080/ping
pong
```

　docker コマンドでネットワークも見てみます

```shell
$ docker network ls
NETWORK ID     NAME                                      DRIVER    SCOPE
b177407351c4   bridge                                    bridge    local
158aba97f065   host                                      host      local
83d9f6d78a10   none                                      null      local
d0c0268df987   web-backend-practice_default              bridge    local
```

　api と client が同一の bridge ネットワークに接続されているということがわかります。DNS が設定されていて `api` や `client` のホスト名を IP アドレスに解決してくれるみたいです。

### nginx を導入

```yaml:docker-compose.yml
# 追記
services: 
  nginx:
    image: nginx:latest
    ports:
      - target: 3000
        published: 3000
    depends_on:
      - api
    tty: true
    stdin_open: true
```

　この状態で `localhost:3000` にアクセスすると nginx の welcome ページが表示されます。次にリバースプロキシを設定して `/api` へのアクセスを API サーバーへ転送するようにします。設定ファイルを記述し `/etc/nginx/conf.d` にマウントします。

```nginx:nginx/server.conf
server {
    listen 3000;
    server_name localhost;

    location /api/ {
        proxy_pass http://api:8080/;
    }
}
```

 ```diff:docker-compose.yml
services: 
  nginx:
    image: nginx:latest
    ports:
      - target: 3000
        published: 3000
+   volumes:
+     - type: bind
+       source: './nginx'
+       target: '/etc/nginx/conf.d'
    depends_on:
      - api
    tty: true
    stdin_open: true
```

:::note
`proxy_pass` の最後にスラッシュ `/` をつけると `/api/foo/bar` が `api:8080/foo/bar` へ転送されるようになります。スラッシュを付けないと `api:8080/api/foo/bar` へ転送されるようになります。私はこれに 15 分ぐらい嵌りました。
:::

　ここまで設定すると `localhost:3000/api/ping` へのアクセスで pong と返ってくるようになります。

## 3. DB を追加する

### Postgres を導入する

 　続いて DBMS を追加します。今回は PostgreSQL を使用します。理由はなんとなくモダンな気がするからです。まずは公式の Postgres イメージを用いて `db` サービスを追加します。DB サーバーのユーザー名、パスワード、データベース名は `.env` ファイルに書いておきます (Compose は勝手に `.env` ファイルを読み込んでくれます)。`POSTGRES_USER`, `POSTGRES_DB` 環境変数を設定しておくことで DB サーバー起動時にユーザー・データベースがあるかを調べない場合には作成してくれます。

```yaml:docker-compose.yaml
# 追記
db:
  image: postgres:16
  ports:
    - target: 5432
      published: 5432
    environment:
      POSTGRES_USER: $POSTGRES_USER
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
      POSTGRES_DB: $POSTGRES_DB
```

次に `api` サービスと `api-dev` サービスにも環境変数の設定を追加します。nginx の設定時と同様にホスト名に `db` を指定すると立ち上がっている postgres コンテナに接続できます。

```diff:docker-compose.yml
  services:
    api:
      build:
        context: './api'
        dockerfile: './Dockerfile'
        target: prod
+     depends_on:
+       - db
+     environment:
+       DB_HOST: db
+       DB_PORT: 5432
+       DB_USER: $POSTGRES_USER
+       DB_PASS: $POSTGRES_PASSWORD
+       DB_NAME: $POSTGRES_DB
      ports:
        - target: 8080
          published: 8080
  # ...
    api-dev:
      build:
        context: './api'
        dockerfile: './Dockerfile'
        target: dev
      volumes:
        - type: bind
          source: ./api
          target: /api
+     depends_on:
+       - db
+     environment:
+       PORT: 8081
+       DB_HOST: db
+       DB_PORT: 5432
+       DB_USER: $POSTGRES_USER
+       DB_PASS: $POSTGRES_PASSWORD
+       DB_NAME: $POSTGRES_DB
      working_dir: /api
      ports:
        - target: 8081
          published: 8081
  # ...
```

## 4. API を作成する

　次に API サーバーに DB を使う機能を実装していきます。まずはできるだけ簡単な実装をするために ORM や大規模なルーターライブラリを使わずに実装していきます。今回はパスパラメータを用意に扱えるようにするために [`gorilla/mux`](gorilla/mux) を使用します。

### サーバーの設計 (概要)

　Repository モデルでは、サーバーを次のようなコンポーネントに階層的に分離して構成します。

- **Router**: HTTP リクエストをパスなどに基づいて Controller に振り分ける
- **Controller (Handler)**: HTTP リクエストのパース・レスポンスの作成を行う
    - 処理の詳細は Usecase に分離する
- **Usecase**: 各リクエストに対応する一連の処理を行う
- **Repository**: DB に対する処理を行う

例えばユーザーと商品、ユーザーの購入した商品の発送状況を管理する API は次のように構成することができそうです。

```mermaid
flowchart TD
    Router --> c1[Controller /users] --> u1[Usecase ユーザー]
    Router --> c2[Controller /items] --> u2[Usecase 商品]
    Router --> c3[Controller /shippings] --> u3[Usecase 発送]
    u1 --> r1[Repository ユーザー]
    u2 --> r2[Repository 商品]
    u3 --> r3[Repository 発送]
    u3 --> r1
    u3 --> r2
```

### サーバーの扱うデータのモデルを設計

　今回は ToDo アプリを作ってみます。API の使用者から見たユーザーと ToDo を次のように定義します。ユーザーは `name` によって識別され、ToDo は `ref` によって識別されます。

```mermaid
classDiagram
    User: +string name
    User: +string display_name

    Todo: +string ref
    Todo: +string user
    Todo: +string title
    Todo: +bool done

    User "1" --> "*" Todo
```

### データベースの設計

　データベースでは API の外側とは異なりユーザー・ToDo は primary key である `id` によって識別するようにします。これは `id` が `SERIAL` (連番; MySQL でいう `AUTO INCREMENT`) であることから `id` が推測されるのを回避するためです。
　今回は DB の migration ファイルのような形で作成します。このスクリプトは冪等 (ソフトウェアの文脈での冪等 = 何度実行しても結果が変わらない) です。

```bash
#!/bin/bash
set -euo pipefail

function query {
    psql -d "$POSTGRES_DB" -U "$POSTGRES_USER" -w -c "$1"
}

query 'DROP TABLE IF EXISTS users CASCADE;'
query 'CREATE TABLE IF NOT EXISTS users(
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL UNIQUE,
    display_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);'

query 'DROP TABLE IF EXISTS todos CASCADE;'
query 'CREATE TABLE IF NOT EXISTS todos ( 
    id SERIAL PRIMARY KEY,
    ref UUID DEFAULT gen_random_uuid(),
    user_id INTEGER NOT NULL,
    title VARCHAR(255) NOT NULL,
    done BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);'
```

### API の設計

　API は次のように設計します。

| メソッド | エンドポイント | 機能 | body |
| -------- | -------- | -------- | -------- |
| (any) | `/ping` | `pong と返答する | |
| POST | `/users` | ユーザーを作成 | `{"name": string, "display_name": string}` |
| DELETE | `/users/{userName}` | ユーザーを削除 | |
| GET | `/users/{userName}/todos` | ユーザーの ToDo 一覧を取得 | |
| POST | `/users/{userName}/todos` | ユーザーの ToDo を作成 | `{"name": string}` |
| PATCH | `/users/{userName}/todos/{todoRef}` | ToDo の状態を更新 | `{"done": boolean }` |
| DELETE | `/users/{userName}/todos/{todoRef}` | ToDo を削除 | |

### モデルの作成

　Go のコードでは DB に対応する `entity.User`, `entity.Todo` と API に対応する `model.User`, `model.Todo` を作成します。

### repository の作成

　ユーザーの ToDo 一覧を取得する機能を例として見ていきます。repository は DB を操作する役割を持っており、このメソッドはユーザーの ID を指定して ToDo の一覧を返します。ポイントは次の点です。

- `sql.DB.Query` で複数行のクエリを発行できる
    - クエリ内に `$1` のように指定して可変長引数で実際の値を指定する
        - SQL injection 対策
    - `sql.DB.QueryRow` で単一行のクエリを発行できる
- `sql.Rows.Next` で次の行を選択できる
- `sql.Rows.Scan` で現在の行をパースできる
    - `sql.Rows.Scan` でパースする順番は `sql.DB.Query` で指定した順になる

```golang
type TodoRepository struct {
	db *sql.DB
}

func (r *TodoRepository) ListByUserID(userID uint) ([]entity.Todo, error) {
    // クエリを発行
	rows, err := r.db.Query(`SELECT id, ref, user_id, title, done FROM todos WHERE user_id = $1;`, userID)
	if err != nil {
		return nil, err
	}

    // クエリの結果の各行をパース
	todos := make([]entity.Todo, 0)
	for rows.Next() {
		todo := entity.Todo{}
		if err = rows.Scan(&todo.ID, &todo.Ref, &todo.UserID, &todo.Title, &todo.Done); err != nil {
			return nil, err
		}

		todos = append(todos, todo)
	}

	return todos, nil
}
```

### usecase の作成

　次に usecase を見ていきます。usecase では API の入力である `userName` を受け取って API の出力である `model.Todo` のリストを返しています。複数の repository の機能を呼び出すことで一連の処理を実装しています。

```golang
type TodoUsecase struct {
	userRepo UserRepository
	todoRepo TodoRepository
}

func (u *TodoUsecase) ListByUser(userName string) ([]model.Todo, error) {
    // ユーザー名からユーザー ID を取得
	user, err := u.userRepo.GetByName(userName)
	if err == sql.ErrNoRows {
		return nil, NoUserError
	} else if err != nil {
		return nil, err
	}

    // ユーザー ID から ToDo 一覧を取得
	todoEntities, err := u.todoRepo.ListByUserID(user.ID)
	if err != nil {
		return nil, err
	}

    // ToDo の entity から model に変換
	todos := make([]model.Todo, 0, len(todoEntities))
	for _, todoEntity := range todoEntities {
		todos = append(todos, *model.NewTodoFromEntity(&todoEntity, userName))
	}

	return todos, nil
}
```

### handler (contoller) を作成

　handler では HTTP リクエストを処理しますが、実装の詳細は usecase に投げます。以下の例は少々複雑ですが、ToDo 一覧機能に対して `net/http` の `http.Handler` を実装しています。

```golang
type ListByUserNameHandler struct {
	todoUsecase TodoUsecase
	UserNameKey string
}

func NewListByUserNameHandler(todoUsecase TodoUsecase, userNameKey string) ListByUserNameHandler {
	return ListByUserNameHandler{
		todoUsecase: todoUsecase,
		UserNameKey: userNameKey,
	}
}

func (h ListByUserNameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // パスパラメータの解析
	vars := mux.Vars(r)

	userName := vars[h.UserNameKey]
	if userName == "" {
		handler.RespondError(w, "User name must not be empty.", http.StatusBadRequest)
		return
	}

    // ビジネスロジックは usecase に隠蔽
	todos, err := h.todoUsecase.ListByUser(userName)
	if err == NoUserError {
		handler.RespondError(w, "User not found.", http.StatusNotFound)
		return
	} else if err != nil {
		log.Printf("Failed to get user. %v", err)
		handler.RespondError(w, "Failed to get user.", http.StatusInternalServerError)
		return
	}

    // JSON を 200 で返す
	res, err := json.Marshal(todos)
	if err != nil {
		log.Printf("Failed to marshal JSON: %v", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
	w.Write(res)
}
```

### リクエストしてみる

　他の機能も実装していき簡易的なスクリプトで思った通りの結果になっているかを確認します。

```bash
PROTOCOL="${PROTOCOL:-http}"
HOST="${HOST:-localhost:8080}"
ORIGIN="$PROTOCOL://$HOST"

# ユーザーを作成
curl -sSL \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"name": "user1", "display_name": "User 1"}' \
    "$ORIGIN/users"

# ToDo を作成
curl -sSL \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"title": "Todo 1"}' \
    "$ORIGIN/users/user1/todos"

# ToDo 一覧を取得
curl -sSL \
    -X GET \
    "$ORIGIN/users/user1/todos"
```

## 5. 負荷テスト

 　K6 を使って負荷テストをしてみます。

 https://grafana.com/docs/k6/latest/

 　細かい実装やフロントより先に k6 を使ってみたかったのでやります。まずは `/api/ping` に対する負荷テストを書いてみます。GitHub からバイナリをダウンロードし、`k6 new ping.js` でテンプレートを生成します (Docker にすればよかったですね)。

  次の設定ファイルはユーザー数が `0 -(10秒)→ 100 -(10秒)→ 200 -(10秒)→ 0` と変化するように設定したものです。`options` に設定したものはコマンドライン引数で上書きできるようなので、複数のテストで同じケースにすることができそうですね。

```javascript:k6/ping.js
import http from "k6/http";
import { sleep } from "k6";

export const options = {
  stages: [
    { duration: "10s", target: 100 },
    { duration: "10s", target: 200 },
    { duration: "10s", target: 0 },
  ],
};

export default function () {
  http.get("http://localhost:3000/api/ping");
  sleep(1);
}
```

実行結果

```

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

     execution: local
        script: ping.js
        output: -

     scenarios: (100.00%) 1 scenario, 200 max VUs, 1m0s max duration (incl. graceful stop):
              * default: Up to 200 looping VUs for 30s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)


     data_received..................: 520 kB 17 kB/s
     data_sent......................: 276 kB 9.1 kB/s
     http_req_blocked...............: avg=65.95µs min=625ns    med=4.79µs  max=37.13ms p(90)=14.38µs p(95)=284.98µs
     http_req_connecting............: avg=49.92µs min=0s       med=0s      max=37.02ms p(90)=0s      p(95)=196.66µs
     http_req_duration..............: avg=2.54ms  min=507.41µs med=1.93ms  max=13.99ms p(90)=4.82ms  p(95)=5.82ms
       { expected_response:true }...: avg=2.54ms  min=507.41µs med=1.93ms  max=13.99ms p(90)=4.82ms  p(95)=5.82ms
     http_req_failed................: 0.00%  ✓ 0          ✗ 3134
     http_req_receiving.............: avg=78.46µs min=4.14µs   med=52.86µs max=3.1ms   p(90)=159.8µs p(95)=229.75µs
     http_req_sending...............: avg=31.5µs  min=2.21µs   med=17.73µs max=1.58ms  p(90)=62.26µs p(95)=104.86µs
     http_req_tls_handshaking.......: avg=0s      min=0s       med=0s      max=0s      p(90)=0s      p(95)=0s
     http_req_waiting...............: avg=2.43ms  min=486.89µs med=1.85ms  max=13.6ms  p(90)=4.67ms  p(95)=5.67ms
     http_reqs......................: 3134   103.412894/s
     iteration_duration.............: avg=1s      min=1s       med=1s      max=1.03s   p(90)=1s      p(95)=1s
     iterations.....................: 3134   103.412894/s
     vus............................: 14     min=14       max=199
     vus_max........................: 200    min=200      max=200


running (0m30.3s), 000/200 VUs, 3134 complete and 0 interrupted iterations
default ✓ [======================================] 000/200 VUs  30s
```

送られたHTTP リクエスト数が 3134、平均待ち時間が 2.54ms、失敗したリクエストの数が 0 であることなどがわかります。

　今度は TODO リストを取得する負荷テストを書いてみます。`setup` 関数で初期化処理、`teardown` 関数で終了処理を書くことができます。初期化処理は VU 共通のため複数ユーザーを生成しておきます。

```k6/listTodo.js
import http from "k6/http";
import { sleep } from "k6";

export const options = {
  stages: [
    { duration: "10s", target: 100 },
    { duration: "10s", target: 200 },
    { duration: "10s", target: 0 },
  ],
};

const numUsers = 10;
const numTodos = 10;

export function setup() {
  const userIds = [];
  for (let i = 0; i < numUsers; i++) {
    const res = http.post(`http://localhost:3000/api/users?name=user${i}`);
    const userId = res.json().id;

    if (typeof userId !== "number") {
      throw new Error("userId is not a number");
    }

    for (let i = 0; i < numTodos; i++) {
      http.post(`http://localhost:3000/api/users/${userId}/todos?title=aaa`);
    }

    userIds.push(userId);
  }

  return { userIds };
}

export default function ({ userIds }) {
  const userId = userIds[Math.floor(Math.random() * userIds.length)];

  http.get(`http://localhost:3000/api/users/${userId}/todos`);

  sleep(1);
}
```

　試しに VU 数を増やしてみると nginx のワーカー数が足りなくなるといった挙動が観察でき面白いです。

:::note warn
これ以降は未実装です。
:::

## ORM を使う

- SQLBoiler or gorm

## API 仕様書を使う

- OpenAPI

## Router を使う

- echo

## セッションの実装

- redis を使ってセッションを実装
- TODO へのアクセスをユーザーでフィルタリング
- ログインは実装しない (このユーザーのセッションをくださいって言ったらもらえる)
    - 名前を unique にする

## キャッシュの実装

- redis を使ってキャッシュしてみる

## 自動スケール

- API サーバー分散できるようにする
- 分散 DB を使う (かも)
- 実際に auto scaling するかは微妙

## (おまけ) フロントを作る

- 適当
    - React か Next か Lit か VanillaJS か......
    - Svelte とか Remix 使ってみてもいい
    - Next とか Remix 使うには小さすぎるかも

## (おまけ) Server sent events

- 複数タブで TODO を編集したら同期する
- フロントでの optimistic update が正当化される

## TODO

- DB を永続化する (volume を使う)
- API サーバー起動時に DB サーバーの起動を待つようにする
- nginx 起動時に API サーバー起動を待つようにする or なくても起動するようにする
- CI を書く

## 他にやりたかったこと

- SQL のパフォーマンスチューニング
    - ある程度でかくて複雑な DB じゃないとできないため
- GraphQL での API 実装
    - graph 使うほどデータ自体が複雑でないため
- AWS などへの CD
    - お金がかかるので......

## 参考記事・文献

 - [GoでWebサーバーを構築](https://zenn.dev/satumahayato010/articles/b4a6cccbb3bb09)
 - [Docker Compose入門 (3) ～ネットワークの理解を深める～ | さくらのナレッジ](https://knowledge.sakura.ad.jp/23899/)
 - [docker-composeでNginx（リバースプロキシ）を立てて、ローカルでHTTPS通信をやってみた ｜SHIFT Group 技術ブログ](https://note.shiftinc.jp/n/n7d5113e5e471)
 - [Nginxのproxy_passを設定する時の注意点 #nginx - Qiita](https://qiita.com/lijiming/items/e5d6ac011d044a02aed1)
- [Go の net/http で Web サーバーを立てる #初心者 - Qiita](https://qiita.com/BitterBamboo/items/6119f7986a04c5a0ac57)
- [go - How to execute an IN lookup in SQL using Golang? - Stack Overflow](https://stackoverflow.com/questions/20271123/how-to-execute-an-in-lookup-in-sql-using-golang)
- [insert - PostgreSQL function for last inserted ID - Stack Overflow](https://stackoverflow.com/questions/2944297/postgresql-function-for-last-inserted-id)
- [database/sql で INSERT したレコードのIDを取得する #Go - Qiita](https://qiita.com/mizumizue/items/12d504eead84214af0e1)
- [負荷テストツール「k6」入門](https://zenn.dev/pharmax/articles/98ed49994cdaf2)
- [Test lifecycle | Grafana k6 documentation](https://grafana.com/docs/k6/latest/using-k6/test-lifecycle/)