0
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?

Go製APIのHTTP POST設計: モデルとハンドラの連携、入力バリデーションとエラーハンドリング

Posted at

はじめに

本記事は個人開発でGo言語を使用してAPI開発をした際のコミットした内容に基づいて生成AIから文章を生成した内容を一部書き加えたものになります

このAPIでできること

  • エンドポイント: POST /sample_records
  • 目的: 指定ユーザーのサンプル記録を1件作成する
  • 依存ルール: ユーザーは事前に users テーブルへ登録されている必要あり

使い方

リクエスト(JSON)

{ "user_id": "sample123", "clock_out_time": "2025-08-22T18:30:00+09:00" }

コマンド例

curl -X POST http://localhost:8080/sample_records \
  -H "Content-Type: application/json" \
  -d '{"user_id":"sample123","clock_out_time":"2025-08-22T18:30:00+09:00"}'

成功(201)

{ "id": 1, "user_id": "sample123", "clock_out_time": "2025-08-22T09:30:00Z" }

入力チェック(何を見ているか)

  • 形式チェック(モデル定義)
    • user_id: 必須・英数字のみ
    • clock_out_time: 必須・RFC3339(タイムゾーン必須)
  • JSONの入り口制御(ハンドラ)
    • 想定外の項目を拒否(DisallowUnknownFields()
    • (必要に応じて)サイズ上限を設定可能(http.MaxBytesReader
  • ドメインルール(サービス)
    • usersテーブルに該当ユーザーが存在すること

実装例

以下はこのドキュメントで述べた設計を最小構成で実装した例です。

  • モデル(入力スキーマ&形式検証)
// model/request.go
type CreateSampleRecordRequest struct {
    UserID       string `json:"user_id" validate:"required,alphanum"`
    ClockOutTime string `json:"clock_out_time" validate:"required,datetime=2006-01-02T15:04:05Z07:00"`
}
  • ハンドラ(JSON入力防御、バリデーション、サービス呼び出し)
// handler/sample_record_handler.go
func (h *SampleRecordHandler) CreateSampleRecord(w http.ResponseWriter, r *http.Request) {
    var req model.CreateSampleRecordRequest

    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields() // 想定外の項目を拒否
    if err := dec.Decode(&req); err != nil {
        if msg := err.Error(); strings.HasPrefix(msg, "json: unknown field ") {
            field := strings.TrimPrefix(msg, "json: unknown field ") 
            http.Error(w, "想定外の項目 " + field + " が含まれています", http.StatusBadRequest)
            return
        }
        http.Error(w, "Invalid JSON format", http.StatusBadRequest)
        return
    }

    if err := h.validator.Struct(req); err != nil { // フィールド検証
        http.Error(w, "Validation failed", http.StatusBadRequest)
        return
    }
    if !h.service.UserExists(req.UserID) { // ドメインルール
        http.Error(w, "User not registered", http.StatusBadRequest)
        return
    }

    record, err := h.service.CreateSampleRecord(req.UserID, req.ClockOutTime)
    if err != nil {
        http.Error(w, "Failed to create sample record", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(record)
}
  • サービス(時刻の正規化と保存委譲)
// service/sample_record_service.go
func (s *sampleRecordService) CreateSampleRecord(userID, clockOutTime string) (*model.SampleRecord, error) {
    const layout = "2006-01-02T15:04:05Z07:00" // RFC3339(TZ必須)
    t, err := time.Parse(layout, clockOutTime)
    if err != nil {
        return nil, err
    }
    return s.repo.CreateSampleRecord(userID, t)
}

エラーの返し方(よくある例)

  • 400 Bad Request: JSONが壊れている/想定外の項目がある
    • 例: 想定外の項目 "extra" が含まれています
  • 422 Unprocessable Entity: 値がルールに合わない(例: clock_out_timeがRFC3339でない)
  • 400 Bad Request: ユーザー未登録(User not registered
  • 409 Conflict: 同一ユーザー・同一時刻の重複(DB一意制約を入れた場合)
  • 500 Internal Server Error: 保存に失敗

設計のポイント(最小で運用しやすく)

  • JSONは「想定外の項目を拒否」して入力ミスに早く気づく
  • 時刻はRFC3339で受け取り、内部ではtime.Parseで正規化
  • ユーザー存在チェックはアプリ側で行い、重複防止はDBの一意制約で守る
  • エラーメッセージは“どう直せば良いか”が分かる表現にする

参考(簡単な流れ)

0
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
0
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?