Go: Go REST…

最終更新:2026-08-26

REST APIは単なるCRUDにとどまりません。リソースの設計、ステータスコードの選定、一貫性のあるエラー表示形式、そしてミドルウェアの連鎖――こうした細部の一つひとつが、APIの本番環境レベルの品質を左右します。

APIをフロントエンドアプリケーションとサードパーティサービスの両方から呼び出す必要がある場合、標準化されたエラー形式、適切なステータスコード、そして明確なバージョン管理は、もはや単なる「あれば望ましい」ものではなく、「必須」のものとなります。

1. 学習内容



2. フロントエンド担当者の実体験

(1) 課題:APIのエラー形式がインターフェースごとに異なり、フロントエンドのクラッシュを引き起こしている

Alice社のバックエンドチームとフロントエンドチームは、あるEコマースプロジェクトで協力しています:

「フロントエンドの同僚から、『APIごとにエラーの表示形式が違うよ。ユーザー一覧APIは{"error":"not found"}を返すし、注文APIは{"message":"Order not found","code":404}を返すし、商品APIはただ500エラーページを表示するだけだ。APIごとに異なるエラー処理コードを書かなきゃいけないじゃないか!』と言われた。」

GO
// Bad code: inconsistent error formats
// GET /users/1 → {"error":"not found"}            ← Format A
// GET /orders/1 → {"message":"Order not found","code":404}  ← Format B
// GET /products → <html>500 Internal Error</html>            ← Format C

(2) Goの解決策:標準化されたエラー応答

GO
// Unified error format
type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Detail  string `json:"detail,omitempty"`
}

func (e *APIError) Error() string {
    return e.Message
}

// Factory functions
func NotFound(msg string) *APIError {
    return &APIError{Code: 404, Message: "not_found", Detail: msg}
}

func BadRequest(msg string) *APIError {
    return &APIError{Code: 400, Message: "bad_request", Detail: msg}
}

func InternalError(msg string) *APIError {
    return &APIError{Code: 500, Message: "internal_error", Detail: msg}
}

(3) 歳入:統一前と統一後

次元 不統一 統一形式
フロントエンド処理 APIごとに異なるロジックを実装 if (resp.error) handleError(resp)
ドキュメント作成コスト APIごとに個別のドキュメント 一文での説明形式
SDKの生成 自動化不可 OpenAPI経由でクライアントを直接生成
デバッグにかかるコスト 毎回具体的な形式を確認する 標準化されたフィールド名


3. RESTful設計の原則

(1) リソースの設計

GO
// Good RESTful URL design:
// Resource (noun) + Verb (HTTP method)

// Single resource
GET    /users          → List (collection)
POST   /users          → Create
GET    /users/{id}     → View single
PUT    /users/{id}     → Full update
PATCH  /users/{id}     → Partial update
DELETE /users/{id}     → Delete

// Sub-resource
GET    /users/{id}/orders      → User's order list
POST   /users/{id}/orders      → Create order for user
GET    /users/{id}/orders/{oid} → User's specific order

// Actions (use verbs for non-CRUD)
POST   /users/{id}/activate    → Activate user
POST   /orders/{id}/cancel     → Cancel order

(2) ステータスコードの選択

GO
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// Unified response
type Response struct {
    Data  interface{} `json:"data,omitempty"`
    Error *APIError   `json:"error,omitempty"`
    Meta  *Meta       `json:"meta,omitempty"`
}

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Detail  string `json:"detail,omitempty"`
}

type Meta struct {
    Total   int `json:"total"`
    Page    int `json:"page"`
    PerPage int `json:"per_page"`
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func main() {
    fmt.Println("Response types: Response, APIError, Meta")
    fmt.Println("Helper: writeJSON(w, status, data)")
}
ステータスコード メソッド 意味
200 OK GET クエリが成功しました
201 作成済み POST 正常に作成されました
204 コンテンツなし 削除 削除に成功しました
400 Bad Request — 無効なリクエストパラメータ
401 アクセス拒否 — アクセス拒否
403 アクセス禁止 — アクセス権限なし
404 Not Found — リソースが存在しません
409 競合 POST/PUT リソースの競合(例:重複作成)
422 処理不可 POST/PUT リクエスト本文のセマンティックエラー
429 数を超過 — レート制限
500 内部エラー — サーバーエラー


4. REST APIの実践

▶ サンプル:ユーザー CRUD API

⚙️ 前提条件: go get github.com/mattn/go-sqlite3 を実行する(SQLite を使用する場合)

GO 📖 参照専用
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "sync"
    "time"
)

// ---------- Model ----------

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

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UpdateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// ---------- Store ----------

type UserStore struct {
    mu     sync.RWMutex
    users  map[int]User
    nextID int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *UserStore) List() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]User, 0, len(s.users))
    for _, u := range s.users {
        result = append(result, u)
    }
    return result
}

func (s *UserStore) Create(req CreateUserRequest) User {
    s.mu.Lock()
    defer s.mu.Unlock()
    u := User{
        ID:        s.nextID,
        Name:      req.Name,
        Email:     req.Email,
        CreatedAt: time.Now(),
    }
    s.nextID++
    s.users[u.ID] = u
    return u
}

func (s *UserStore) Get(id int) (User, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    u, ok := s.users[id]
    return u, ok
}

func (s *UserStore) Update(id int, req UpdateUserRequest) (User, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    u, ok := s.users[id]
    if !ok {
        return User{}, false
    }
    u.Name = req.Name
    u.Email = req.Email
    s.users[id] = u
    return u, true
}

func (s *UserStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.users[id]
    if !ok {
        return false
    }
    delete(s.users, id)
    return true
}

// ---------- Handler ----------

type UserHandler struct {
    store *UserStore
}

func NewUserHandler(store *UserStore) *UserHandler {
    return &UserHandler{store: store}
}

func (h *UserHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/v1/users", h.ListUsers)
    mux.HandleFunc("POST /api/v1/users", h.CreateUser)
    mux.HandleFunc("GET /api/v1/users/{id}", h.GetUser)
    mux.HandleFunc("PUT /api/v1/users/{id}", h.UpdateUser)
    mux.HandleFunc("DELETE /api/v1/users/{id}", h.DeleteUser)
}

// Unified response
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]interface{}{
        "error": map[string]interface{}{
            "code":    status,
            "message": message,
        },
    })
}

func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
    users := h.store.List()
    writeJSON(w, http.StatusOK, map[string]interface{}{
        "data": users,
        "meta": map[string]int{"total": len(users)},
    })
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    // Parameter validation
    if req.Name == "" {
        writeError(w, http.StatusBadRequest, "name is required")
        return
    }
    if req.Email == "" {
        writeError(w, http.StatusBadRequest, "email is required")
        return
    }

    user := h.store.Create(req)
    writeJSON(w, http.StatusCreated, map[string]interface{}{
        "data": user,
    })
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid user ID")
        return
    }

    user, ok := h.store.Get(id)
    if !ok {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }

    writeJSON(w, http.StatusOK, map[string]interface{}{
        "data": user,
    })
}

func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid user ID")
        return
    }

    var req UpdateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    user, ok := h.store.Update(id, req)
    if !ok {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }

    writeJSON(w, http.StatusOK, map[string]interface{}{
        "data": user,
    })
}

func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid user ID")
        return
    }

    if !h.store.Delete(id) {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func main() {
    store := NewUserStore()
    handler := NewUserHandler(store)

    mux := http.NewServeMux()
    handler.Register(mux)

    log.Println("User API started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
論理コード 192 行(40 行制限超過、参照専用)

5. パラメータの検証

▶ サンプル:構造化検証

GO 📖 参照専用
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "regexp"
    "strings"
)

// Validator
type Validator struct {
    errors []string
}

func (v *Validator) Required(field, value string) {
    if strings.TrimSpace(value) == "" {
        v.errors = append(v.errors, fmt.Sprintf("%s is required", field))
    }
}

func (v *Validator) MinLength(field, value string, min int) {
    if len(value) < min {
        v.errors = append(v.errors, fmt.Sprintf("%s must be at least %d characters", field, min))
    }
}

func (v *Validator) MaxLength(field, value string, max int) {
    if len(value) > max {
        v.errors = append(v.errors, fmt.Sprintf("%s must be at most %d characters", field, max))
    }
}

func (v *Validator) Email(field, value string) {
    pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
    matched, _ := regexp.MatchString(pattern, value)
    if !matched {
        v.errors = append(v.errors, fmt.Sprintf("%s is not a valid email", field))
    }
}

func (v *Validator) Valid() bool {
    return len(v.errors) == 0
}

func (v *Validator) Errors() []string {
    return v.errors
}

// ---------- Usage ----------

type SignupRequest struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
    Age      int    `json:"age"`
}

func validateSignup(req SignupRequest) *Validator {
    v := &Validator{}
    v.Required("name", req.Name)
    v.MinLength("name", req.Name, 2)
    v.MaxLength("name", req.Name, 50)
    v.Required("email", req.Email)
    v.Email("email", req.Email)
    v.Required("password", req.Password)
    v.MinLength("password", req.Password, 8)
    return v
}

func signupHandler(w http.ResponseWriter, r *http.Request) {
    var req SignupRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    if v := validateSignup(req); !v.Valid() {
        writeJSON(w, http.StatusUnprocessableEntity, map[string]interface{}{
            "error": map[string]interface{}{
                "code":    422,
                "message": "validation failed",
                "details": v.Errors(),
            },
        })
        return
    }

    writeJSON(w, http.StatusCreated, map[string]string{"status": "ok"})
}

func main() {
    v := validateSignup(SignupRequest{Name: "A", Email: "bad", Password: "123"})
    if !v.Valid() {
        fmt.Println("Validation errors:", v.Errors())
    }
}
論理コード 80 行(40 行制限超過、参照専用)

6. ミドルウェア・チェーンの統合

▶ サンプル:API ミドルウェアチェーン

GO 📖 参照専用
package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "strings"
    "time"
)

type contextKey string

const UserContextKey contextKey = "user"

// ---------- Middleware ----------

type Middleware func(http.Handler) http.Handler

func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

// Request logging
func RequestLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
        next.ServeHTTP(w, r)
        log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// Auth (simple Token)
func Auth(token string) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            auth := r.Header.Get("Authorization")
            if !strings.HasPrefix(auth, "Bearer ") {
                writeError(w, http.StatusUnauthorized, "missing or invalid token")
                return
            }
            if auth[7:] != token {
                writeError(w, http.StatusForbidden, "invalid token")
                return
            }
            // Inject user info into Context
            ctx := context.WithValue(r.Context(), UserContextKey, "admin")
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// Request timeout
func RequestTimeout(timeout time.Duration) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx, cancel := context.WithTimeout(r.Context(), timeout)
            defer cancel()
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// CORS
func CORS(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Recovery
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("[PANIC] %v", err)
                writeError(w, http.StatusInternalServerError, "internal server error")
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// Helper functions
func writeError(w http.ResponseWriter, status int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "error": map[string]interface{}{
            "code":    status,
            "message": msg,
        },
    })
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

// ---------- Main program ----------

func helloHandler(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value(UserContextKey)
    writeJSON(w, http.StatusOK, map[string]interface{}{
        "message": "Hello, " + user.(string),
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/hello", helloHandler)

    // Middleware chain
    handler := Chain(mux,
        Recovery,
        RequestLogging,
        CORS,
        Auth("secret-token"),
        RequestTimeout(5*time.Second),
    )

    log.Println("API started on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}
論理コード 109 行(40 行制限超過、参照専用)

7. 完全な例:図書館管理システム API

GO
// book_api.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
    "time"
)

// ---------- Models ----------

type Book struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Author    string    `json:"author"`
    ISBN      string    `json:"isbn"`
    Year      int       `json:"year"`
    Available bool      `json:"available"`
    CreatedAt time.Time `json:"created_at"`
}

type CreateBookRequest struct {
    Title  string `json:"title"`
    Author string `json:"author"`
    ISBN   string `json:"isbn"`
    Year   int    `json:"year"`
}

type UpdateBookRequest struct {
    Title     string `json:"title"`
    Author    string `json:"author"`
    Available *bool  `json:"available"`
}

// ---------- Validator ----------

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

func validateCreateBook(req CreateBookRequest) []ValidationError {
    var errs []ValidationError
    if strings.TrimSpace(req.Title) == "" {
        errs = append(errs, ValidationError{"title", "title is required"})
    }
    if strings.TrimSpace(req.Author) == "" {
        errs = append(errs, ValidationError{"author", "author is required"})
    }
    if strings.TrimSpace(req.ISBN) == "" {
        errs = append(errs, ValidationError{"isbn", "ISBN is required"})
    }
    if req.Year < 1000 || req.Year > 2100 {
        errs = append(errs, ValidationError{"year", "year must be between 1000 and 2100"})
    }
    return errs
}

// ---------- Store ----------

type BookStore struct {
    mu     sync.RWMutex
    books  map[int]Book
    nextID int
}

func NewBookStore() *BookStore {
    return &BookStore{
        books:  make(map[int]Book),
        nextID: 1,
    }
}

func (s *BookStore) List() []Book {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]Book, 0, len(s.books))
    for _, b := range s.books {
        result = append(result, b)
    }
    return result
}

func (s *BookStore) Create(req CreateBookRequest) (Book, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    // Check ISBN uniqueness
    for _, b := range s.books {
        if b.ISBN == req.ISBN {
            return Book{}, fmt.Errorf("ISBN already exists: %s", req.ISBN)
        }
    }

    book := Book{
        ID:        s.nextID,
        Title:     req.Title,
        Author:    req.Author,
        ISBN:      req.ISBN,
        Year:      req.Year,
        Available: true,
        CreatedAt: time.Now(),
    }
    s.nextID++
    s.books[book.ID] = book
    return book, nil
}

func (s *BookStore) Get(id int) (Book, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    b, ok := s.books[id]
    return b, ok
}

func (s *BookStore) Update(id int, req UpdateBookRequest) (Book, bool, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    b, ok := s.books[id]
    if !ok {
        return Book{}, false, nil
    }
    if req.Title != "" {
        b.Title = req.Title
    }
    if req.Author != "" {
        b.Author = req.Author
    }
    if req.Available != nil {
        b.Available = *req.Available
    }
    s.books[id] = b
    return b, true, nil
}

func (s *BookStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.books[id]
    if !ok {
        return false
    }
    delete(s.books, id)
    return true
}

// ---------- API Response ----------

type APIResponse struct {
    Data  interface{} `json:"data,omitempty"`
    Error *APIError   `json:"error,omitempty"`
    Meta  *Meta       `json:"meta,omitempty"`
}

type APIError struct {
    Code    int               `json:"code"`
    Message string            `json:"message"`
    Details []ValidationError `json:"details,omitempty"`
}

type Meta struct {
    Total int `json:"total"`
}

func respond(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Data: data})
}

func respondError(w http.ResponseWriter, status int, msg string, details ...[]ValidationError) {
    err := APIError{Code: status, Message: msg}
    if len(details) > 0 {
        err.Details = details[0]
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Error: &err})
}

// ---------- Handlers ----------

type BookHandler struct {
    store *BookStore
}

func NewBookHandler(store *BookStore) *BookHandler {
    return &BookHandler{store: store}
}

func (h *BookHandler) Register(mux *http.ServeMux, basePath string) {
    mux.HandleFunc("GET "+basePath, h.ListBooks)
    mux.HandleFunc("POST "+basePath, h.CreateBook)
    mux.HandleFunc("GET "+basePath+"/{id}", h.GetBook)
    mux.HandleFunc("PUT "+basePath+"/{id}", h.UpdateBook)
    mux.HandleFunc("DELETE "+basePath+"/{id}", h.DeleteBook)
}

func (h *BookHandler) ListBooks(w http.ResponseWriter, r *http.Request) {
    books := h.store.List()
    respond(w, http.StatusOK, map[string]interface{}{
        "items": books,
        "meta":  Meta{Total: len(books)},
    })
}

func (h *BookHandler) CreateBook(w http.ResponseWriter, r *http.Request) {
    var req CreateBookRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    if errs := validateCreateBook(req); len(errs) > 0 {
        respondError(w, http.StatusUnprocessableEntity, "validation failed", errs)
        return
    }

    book, err := h.store.Create(req)
    if err != nil {
        respondError(w, http.StatusConflict, err.Error())
        return
    }

    respond(w, http.StatusCreated, book)
}

func (h *BookHandler) GetBook(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        respondError(w, http.StatusBadRequest, "invalid book ID")
        return
    }

    book, ok := h.store.Get(id)
    if !ok {
        respondError(w, http.StatusNotFound, "book not found")
        return
    }

    respond(w, http.StatusOK, book)
}

func (h *BookHandler) UpdateBook(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        respondError(w, http.StatusBadRequest, "invalid book ID")
        return
    }

    var req UpdateBookRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    book, ok, err := h.store.Update(id, req)
    if err != nil {
        respondError(w, http.StatusInternalServerError, err.Error())
        return
    }
    if !ok {
        respondError(w, http.StatusNotFound, "book not found")
        return
    }

    respond(w, http.StatusOK, book)
}

func (h *BookHandler) DeleteBook(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        respondError(w, http.StatusBadRequest, "invalid book ID")
        return
    }

    if !h.store.Delete(id) {
        respondError(w, http.StatusNotFound, "book not found")
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

// ---------- Middleware ----------

func RequestLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
        next.ServeHTTP(w, r)
        log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("[PANIC] %v", err)
                respondError(w, http.StatusInternalServerError, "internal server error")
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// ---------- Main ----------

func main() {
    store := NewBookStore()
    handler := NewBookHandler(store)

    mux := http.NewServeMux()
    handler.Register(mux, "/api/v1/books")

    // Health check
    mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
        respond(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    // Middleware chain
    app := Recovery(RequestLogging(mux))

    log.Println("Book API started on :8080")
    log.Println("Endpoints:")
    log.Println("  GET    /api/v1/books       — Book list")
    log.Println("  POST   /api/v1/books       — Create book")
    log.Println("  GET    /api/v1/books/{id}  — View book")
    log.Println("  PUT    /api/v1/books/{id}  — Update book")
    log.Println("  DELETE /api/v1/books/{id}  — Delete book")
    log.Fatal(http.ListenAndServe(":8080", app))
}
100%
graph LR
    subgraph API [REST API Layer]
        H[Handler]
        M[Middleware Chain]
        V[Validator]
    end
    subgraph Store [Store Layer]
        R[Repository<br/>map + RWMutex]
    end
    subgraph Model [Model Layer]
        Book
        User
        Order
    end
    Request --> M
    M --> H
    H --> V
    V --> R
    R --> Book
🔥 よくある間違い: レスポンスタイプとして interface{} を使用する際は注意が必要です。マップのゼロ値は、JSON では {} ではなく null としてシリアライズされます。ベストプラクティスとして、明示的なレスポンス構造体(APIResponse{Data, Error, Meta}など)を定義し、値がある場合はフィールドが表示され、空の場合は省略される(omitempty)ようにすることをお勧めします。


❓ よくある質問

Q RESTful APIのバージョン管理はどのように行いますか?
A 4つの方法があります:(1) URLパス /api/v1/(最も一般的);(2) リクエストヘッダー Accept: application/vnd.api+json; version=1;(3) クエリパラメータ ?v=1; (4) サブドメイン v1.api.example.com。URLパスが推奨されます。これは最も直感的であり、開発およびデバッグのコストも最も低くなります。
Q エラーの表示形式をどのように標準化すればよいでしょうか?
A コード、メッセージ、詳細のフィールドを含む APIError 構造体を定義します。すべてのハンドラは、標準化された respondError(w, status, msg) 関数を使用する必要があります。フロントエンドは、具体的な API を気にすることなく、if resp.error を確認するだけでエラーを表示できます。
Q パラメータの検証は、ハンドラ層で行うべきか、それともサービス層で行うべきか?
A 基本的な検証(必須フィールド、形式)はハンドラ層で行い、ビジネス検証(一意性、権限)はサービス層で行います。ハンドラ層で検証に失敗した場合はステータスコード400または422が返され、サービス層で失敗した場合はステータスコード409または403が返されます。
Q PUTとPATCHの違いは何ですか?
A PUTは完全置換です。クライアントはリソース全体を送信し、欠落しているフィールドはリセットされたものとみなされます。PATCHは部分更新です。クライアントは変更対象のフィールドのみを送信します。実装の観点では、PUTの方が単純ですが、PATCHでは部分的なフィールドの統合処理が必要となります。CRUD操作にはPUTを、複雑な更新にはPATCHを使用することをお勧めします。
Q 404「ルートが見つかりません」エラーにはどのように対処すればよいですか?
A デフォルトでは、ServeMuxは404ページを返します。カスタマイズ方法:キャッチオールハンドラを作成します:mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") })。ServeMuxはベストマッチルーティングを採用しているため、このハンドラは最後に登録する必要がある点に注意してください。
Q ページネーションはどのように実装されていますか?
A クエリパラメータ ?page=1&per_page=20 を使用しています。ハンドラーがパラメータを解析し、ストア層がLIMITとOFFSETを使用してクエリを実行します。レスポンスにはmeta: {total, page, per_page}が返され、フロントエンドがページネーションコンポーネントを計算します。Go 1.22のルーティングにおける{path...}構文はページネーションパラメータには適していません。パラメータはクエリ文字列に記述する必要があります。
Q HATEOAS(Hypermedia-As-a-Service)を実装する必要はありますか?
A いいえ。REST APIの実務において、HATEOASが実際に使用されることはほとんどありません。ほとんどのAPIでは、リソースデータとメタデータを返すだけで十分です。フロントエンドはAPIドキュメントから次に何をすべきかを把握しており、APIから「何ができるか」を指示される必要はありません。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):Author REST API を構築してください。要件:(1) 完全な CRUD 機能;(2) /api/v1/authors パスを使用すること;(3) 標準化されたエラー形式;(4) 基本的なパラメータの検証(name は必須)。curl を使用してすべてのエンドポイントをテストしてください。

  2. 上級演習(難易度 ⭐⭐):記事と著者を関連付ける記事APIを実装してください。要件:(1) POST /articles を使用して記事を作成する(既存の著者と関連付ける); (2) GET /articles?author_id=X を使用して著者を条件にフィルタリングすること;(3) ページネーションをサポートすること(page および per_page パラメータを使用);(4) 一貫したレスポンス形式を使用すること:{data, meta};(5) title および content が空でないことを検証すること。

  3. 課題(難易度:⭐⭐⭐):完全なミドルウェアチェーンを備えたユーザー管理システムを実装してください。要件:(1) ルート:users(CRUD)+ auth(ログイン/登録);(2) ミドルウェア:Recovery → Logging → CORS → RateLimit(トークンバケット)→ Auth(Bearer Token)→ Timeout;(3) 登録時に bcrypt を使用してパスワードを暗号化すること;ログイン時に JWT を返すこと; (4) 認証ミドルウェアは、JWTからuserIDを解析し、それをContextに注入する必要があります;(5) -race を使用して、並行処理の安全性を検証してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%