はじめに
本記事は個人開発で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の一意制約で守る
- エラーメッセージは“どう直せば良いか”が分かる表現にする