じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

Fiber v3 を使ったが変更点を確認だけして手癖で解決した

はじめに

この記事では、プログラミング言語のGoとWebフレームワークであるFiber v3を使って、リレーショナルデータベースのPostgreSQLをバックエンドに利用したCRUD (Create, Read, Update, Delete) 操作ができるWeb APIを作成する方法を説明します。

本来であれば、Fiber v3の新機能や変更点を活用したかったのですが、十分な調査を行う前に雰囲気で実装を進めてしまったため、本記事ではそれらを採用できておりません。深夜に検証を行っていたこともあり、ベストプラクティスとは言えない部分があることを認識しています。

エンジニアとして、新しいバージョンのフレームワークを使う際には、事前に十分な調査を行い、新機能や変更点を理解した上で実装を進めるべきでした。GitHub Copilot とLanguage Server で適当に書いてしまいました。また、コードの品質を保つためには、適切な時間帯に集中して作業を行うことが重要です(これはガチ)。

今回の実装では、これらの点が不十分であったことを反省しています。業務では、技術選定や実装方法について、より慎重に検討を行い、品質の高いコードを書くことを心がけたいと思います。

読者の皆様におかれましては、本記事の内容を参考にする際には、上記の点にご留意いただければ幸いです。

github.com

プロジェクトの初期化

まず、新しいGoプロジェクトを作成します。

mkdir fiber-crud-api
cd fiber-crud-api
go mod init github.com/yourusername/fiber-crud-api

必要なパッケージのインストール

次に、必要なパッケージをインストールします。

go get github.com/gofiber/fiber/v3
go get github.com/lib/pq

データベースの設定とモデルの定義

main.goファイルを作成し、以下のようにデータベースの設定とモデルを定義します。

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/gofiber/fiber/v3"
    _ "github.com/lib/pq"
)

const (
    host     = "db"
    port     = 5432
    user     = "postgres"
    password = "password"
    dbname   = "mydb"
)

// Connect to the database
func Connect() (*sql.DB, error) {
    psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
    db, err := sql.Open("postgres", psqlInfo)
    if err != nil {
        return nil, err
    }
    return db, nil
}

// User model
type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Password  string    `json:"password"`
    CreatedAt time.Time `json:"created_at"`
}

// Post model
type Post struct {
    ID        int       `json:"id"`
    UserID    int       `json:"user_id"`
    Title     string    `json:"title"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

APIエンドポイントの実装

続けて、main.goファイルにAPIエンドポイントを実装します。v2 の時には存在していたctx のbodyparserv3ではなくなっていたので手癖でJSONで返したのですがおそらくBind周りで実装するとよいみたいです(あとから気付きました...)。v2からv3の変更点については以下を参考にしてください。

docs.gofiber.io

func main() {
    app := fiber.New()

    // データベース接続
    db, err := Connect()
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Create User
    app.Post("/users", func(c fiber.Ctx) error {
        user := new(User)
        if err := json.Unmarshal(c.Body(), user); err != nil {
            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                "error": "Invalid request body",
            })
        }

        // パスワードのハッシュ化やバリデーションを行うことが推奨される
        // 簡易実装のため、ここでは省略

        // データベースにユーザーを作成
        _, err := db.Exec("INSERT INTO users (name, email, password) VALUES ($1, $2, $3)", user.Name, user.Email, user.Password)
        if err != nil {
            return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
                "error": "Failed to create user",
            })
        }

        return c.Status(fiber.StatusCreated).JSON(fiber.Map{
            "message": "User created",
        })
    })

    // Get User
    app.Get("/users/:id", func(c fiber.Ctx) error {
        id := c.Params("id")

        // データベースからユーザーを取得
        row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
        user := new(User)
        if err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt); err != nil {
            if err == sql.ErrNoRows {
                return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
                    "error": "User not found",
                })
            }
            return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
                "error": "Failed to get user",
            })
        }

        return c.JSON(user)
    })

    // Create Post
    app.Post("/posts", func(c fiber.Ctx) error {
        post := new(Post)
        if err := json.Unmarshal(c.Body(), post); err != nil {
            return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                "error": "Invalid request body",
            })
        }

        // データベースに記事を作成
        _, err := db.Exec("INSERT INTO posts (user_id, title, content) VALUES ($1, $2, $3)", post.UserID, post.Title, post.Content)
        if err != nil {
            return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
                "error": "Failed to create post",
            })
        }

        return c.Status(fiber.StatusCreated).JSON(fiber.Map{
            "message": "Post created",
        })
    })

    // Get Post
    app.Get("/posts/:id", func(c fiber.Ctx) error {
        id := c.Params("id")

        // データベースから記事を取得
        row := db.QueryRow("SELECT * FROM posts WHERE id = $1", id)
        post := new(Post)
        if err := row.Scan(&post.ID, &post.UserID, &post.Title, &post.Content, &post.CreatedAt, &post.UpdatedAt); err != nil {
            if err == sql.ErrNoRows {
                return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
                    "error": "Post not found",
                })
            }
            return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
                "error": "Failed to get post",
            })
        }

        return c.JSON(post)
    })

    log.Fatal(app.Listen(":3000"))
}

このコードでは、以下のエンドポイントを実装しています。

  • POST /users: 新しいユーザーを作成します。
  • GET /users/:id: 指定されたIDのユーザーを取得します。
  • POST /posts: 新しい記事を作成します。
  • GET /posts/:id: 指定されたIDの記事を取得します。

Dockerfileとdocker-compose.ymlの作成

開発環境用のDockerfiledocker-compose.ymlを作成します。これも簡易的に用意した適当なファイルなので十分に吟味してください。

FROM golang:1.22-alpine

WORKDIR /app

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

COPY . .

RUN go build -o main .

CMD ["./main"]
version: '3'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db
  db:
    image: postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

データベースの初期化スクリプト

init.sqlファイルを作成し、データベースの初期化スクリプトを記述します。

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    title VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

アプリケーションの実行

以下のコマンドを実行してアプリケーションを起動します。

docker-compose up --build

これで、GoとFiberを使ってCRUDができるAPIが作成され、PostgreSQLをバックエンドに利用する環境が整いました。APIエンドポイントをテストするために、cURLやPostmanなどのツールを使用してリクエストを送信できます。

APIのテストとデータベースの確認

アプリケーションを起動した後、CURLを使ってAPIエンドポイントをテストし、PostgreSQLの中身を確認してみましょう。

ユーザーの作成と確認

まず、新しいユーザーを作成します。

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"john@example.com","password":"secret"}' http://localhost:3000/users

レスポンスとして、"User created"が返ってくるはずです。

次に、作成したユーザーを確認します。

curl http://localhost:3000/users/1

レスポンスとして、作成したユーザーの情報がJSON形式で返ってきます。

{"id":1,"name":"John Doe","email":"john@example.com","password":"secret","created_at":"2023-04-24T12:34:56Z"}

PostgreSQLの中身を確認するために、データベースにログインします。

docker-compose exec db psql -U postgres mydb

ユーザーテーブルの中身を確認します。

SELECT * FROM users;

作成したユーザーがテーブルに存在することを確認できます。

 id |  name   |     email      | password |         created_at
----+---------+----------------+----------+----------------------------
  1 | John Doe| john@example.com| secret   | 2024-05-30 01:34:56.789012
(1 row)

記事の作成と確認

次に、新しい記事を作成します。

curl -X POST -H "Content-Type: application/json" -d '{"user_id":1,"title":"My First Post","content":"Hello, World!"}' http://localhost:3000/posts

レスポンスとして、"Post created"が返ってくるはずです。

作成した記事を確認します。

curl http://localhost:3000/posts/1

レスポンスとして、作成した記事の情報がJSON形式で返ってきます。

{"id":1,"user_id":1,"title":"My First Post","content":"Hello, World!","created_at":"2023-04-24T12:45:67Z","updated_at":"2023-04-24T12:45:67Z"}

再度、PostgreSQLの中身を確認します。

SELECT * FROM posts;

作成した記事がテーブルに存在することを確認できます。

 id | user_id |    title     |    content    |         created_at         |         updated_at
----+---------+--------------+---------------+----------------------------+----------------------------
  1 |       1 | My First Post| Hello, World! | 2024-05-30 01:45:67.890123 | 2024-05-30 01:45:67.890123
(1 row)

以上で、APIのテストとデータベースの確認が完了しました。

さいごに

以上が、GoとFiberを使ってCRUDができるAPIを作成し、PostgreSQLをバックエンドに利用する方法です。実際のアプリケーションでは、エラーハンドリングやバリデーションなどを追加し、より堅牢なAPIを作成することが重要です。また、認証や認可、ページネーション、フィルタリングなどの機能も必要になるでしょう。データベースのマイグレーションツールを使用して、テーブル構造の変更を管理することも忘れずに。本当に手癖でやってみただけです。楽しかったです。この記事を書いている時にはaikoを聴いていたのでプレイリストも共有です。

open.spotify.com