Go: Go REST…
REST APIは単なるCRUDにとどまりません。リソースの設計、ステータスコードの選定、一貫性のあるエラー表示形式、そしてミドルウェアの連鎖――こうした細部の一つひとつが、APIの本番環境レベルの品質を左右します。
APIをフロントエンドアプリケーションとサードパーティサービスの両方から呼び出す必要がある場合、標準化されたエラー形式、適切なステータスコード、そして明確なバージョン管理は、もはや単なる「あれば望ましい」ものではなく、「必須」のものとなります。
1. 学習内容
- RESTful設計の原則(リソース/動詞/ステータスコード)
- Go 1.22のRoutesを使用してREST APIを構築する
- リクエストパラメータの検証
- ミドルウェア・チェーンの統合
- エラー応答の形式を標準化する
- APIバージョン管理ポリシー
2. フロントエンド担当者の実体験
(1) 課題:APIのエラー形式がインターフェースごとに異なり、フロントエンドのクラッシュを引き起こしている
Alice社のバックエンドチームとフロントエンドチームは、あるEコマースプロジェクトで協力しています:
「フロントエンドの同僚から、『APIごとにエラーの表示形式が違うよ。ユーザー一覧APIは
{"error":"not found"}を返すし、注文APIは{"message":"Order not found","code":404}を返すし、商品APIはただ500エラーページを表示するだけだ。APIごとに異なるエラー処理コードを書かなきゃいけないじゃないか!』と言われた。」
// 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の解決策:標準化されたエラー応答
// 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) リソースの設計
// 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) ステータスコードの選択
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 を使用する場合)
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))
}
5. パラメータの検証
▶ サンプル:構造化検証
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())
}
}
6. ミドルウェア・チェーンの統合
▶ サンプル:API ミドルウェアチェーン
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))
}
7. 完全な例:図書館管理システム API
// 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))
}
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)ようにすることをお勧めします。
❓ よくある質問
/api/v1/(最も一般的);(2) リクエストヘッダー Accept: application/vnd.api+json; version=1;(3) クエリパラメータ ?v=1; (4) サブドメイン v1.api.example.com。URLパスが推奨されます。これは最も直感的であり、開発およびデバッグのコストも最も低くなります。APIError 構造体を定義します。すべてのハンドラは、標準化された respondError(w, status, msg) 関数を使用する必要があります。フロントエンドは、具体的な API を気にすることなく、if resp.error を確認するだけでエラーを表示できます。mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") })。ServeMuxはベストマッチルーティングを採用しているため、このハンドラは最後に登録する必要がある点に注意してください。?page=1&per_page=20 を使用しています。ハンドラーがパラメータを解析し、ストア層がLIMITとOFFSETを使用してクエリを実行します。レスポンスにはmeta: {total, page, per_page}が返され、フロントエンドがページネーションコンポーネントを計算します。Go 1.22のルーティングにおける{path...}構文はページネーションパラメータには適していません。パラメータはクエリ文字列に記述する必要があります。📖 まとめ
- RESTの設計:リソース(名詞)+メソッド(動詞)+ステータスコード
- URLのバージョン管理:
/api/v1/の手法を強く推奨します - エラー形式を統一する:
{error: {code, message, details}} - パラメータの検証:基本検証(ハンドラ)+ビジネス検証(サービス)
- ミドルウェアチェーン:リカバリ → ロギング → CORS → 認証 → タイムアウト → ハンドラー
- 統一応答関数:
respond()/respondError() - ページネーション:
?page=N&per_page=N+ メタ情報
📝 練習問題
-
基本演習(難易度 ⭐):Author REST API を構築してください。要件:(1) 完全な CRUD 機能;(2)
/api/v1/authorsパスを使用すること;(3) 標準化されたエラー形式;(4) 基本的なパラメータの検証(name は必須)。curl を使用してすべてのエンドポイントをテストしてください。 -
上級演習(難易度 ⭐⭐):記事と著者を関連付ける記事APIを実装してください。要件:(1)
POST /articlesを使用して記事を作成する(既存の著者と関連付ける); (2)GET /articles?author_id=Xを使用して著者を条件にフィルタリングすること;(3) ページネーションをサポートすること(pageおよびper_pageパラメータを使用);(4) 一貫したレスポンス形式を使用すること:{data, meta};(5)titleおよびcontentが空でないことを検証すること。 -
課題(難易度:⭐⭐⭐):完全なミドルウェアチェーンを備えたユーザー管理システムを実装してください。要件:(1) ルート:users(CRUD)+ auth(ログイン/登録);(2) ミドルウェア:Recovery → Logging → CORS → RateLimit(トークンバケット)→ Auth(Bearer Token)→ Timeout;(3) 登録時に bcrypt を使用してパスワードを暗号化すること;ログイン時に JWT を返すこと; (4) 認証ミドルウェアは、JWTからuserIDを解析し、それをContextに注入する必要があります;(5)
-raceを使用して、並行処理の安全性を検証してください。