Go: Go REST API in Practice

A REST API is more than just CRUD—resource design, حالة code selection, consistent خطأ formatting, and وسيط chains: every detail determines the production-grade quality of the API.

When your API needs to be called by both front-end applications and third-party services, a standardized خطأ format, appropriate حالة codes, and clear version management are no longer just "nice-to-haves"—they are "must-haves."

1. You will learn



2. A True Story from a Front-End Collaborator

(1) Pain Point: API خطأ formats vary by interface, causing front-end crashes

Alice's واجهة خلفية and واجهة أمامية teams are collaborating on an e-commerce project:

"My front-end colleagues said, 'The خطأ formats for each of your APIs are different. The user list returns {"خطأ":"not found"}, the order API returns {"message":"Order not found","code":404}, and the product API just returns a 500 خطأ page. I have to write different خطأ-handling code for each 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 Solution: Standardized Error Responses

GO
// Unified خطأ format
type APIError struct {
    Code    int    `json:"code"`
    Message سلسلة `json:"message"`
    Detail  سلسلة `json:"detail,omitempty"`
}

func (e *APIError) Error() سلسلة {
    return e.Message
}

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

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

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

(3) Revenue: Before and After Unification

Dimension Inconsistent Unified Format
Front-end processing Write different logic for each API if (resp.error) handleError(resp)
Documentation Cost Separate documentation for each API One-sentence description format
SDK Generation Cannot Be Automated Directly Generate Client via OpenAPI
Debugging Costs Check the specific format each time Standardized field names


3. RESTful Design Principles

(1) Resource Design

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) Status Code Selection

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)")
}
Status Code Method Meaning
200 OK GET Query successful
201 Created POST Created successfully
204 No Content DELETE Deleted successfully
400 Bad Request Invalid request parameters
401 Unauthorized Unauthorized
403 Forbidden No Permission
404 Not Found Resource does not exist
409 Conflict POST/PUT Resource conflict (e.g., duplicate creation)
422 Unprocessable POST/PUT Request body semantic error
429 Too Many Rate Limit
500 Internal Server Error


4. Hands-On REST API

▶ Example: User CRUD API

⚙️ Prerequisite: Run go get github.com/mattn/go-sqlite3 (if using SQLite)

GO 📖 Display only
package main

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

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

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

type CreateUserRequest struct {
    Name  سلسلة `json:"name"`
    Email سلسلة `json:"email"`
}

type UpdateUserRequest struct {
    Name  سلسلة `json:"name"`
    Email سلسلة `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 استجابة
func writeJSON(w http.ResponseWriter, حالة int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(حالة)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, حالة int, message سلسلة) {
    writeJSON(w, حالة, map[سلسلة]interface{}{
        "خطأ": map[سلسلة]interface{}{
            "code":    حالة,
            "message": message,
        },
    })
}

func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
    users := h.store.List()
    writeJSON(w, http.StatusOK, map[سلسلة]interface{}{
        "data": users,
        "meta": map[سلسلة]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[سلسلة]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[سلسلة]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[سلسلة]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 logic lines (exceeds 40-line limit, display only)

5. Parameter Validation

▶ Example: Structured Validation

GO 📖 Display only
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 logic lines (exceeds 40-line limit, display only)

6. Middleware Chain Integration

▶ Example: API وسيط chain

GO 📖 Display only
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 logic lines (exceeds 40-line limit, display only)

7. Complete Example: Library Management System 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     سلسلة    `json:"title"`
    Author    سلسلة    `json:"author"`
    ISBN      سلسلة    `json:"isbn"`
    Year      int       `json:"year"`
    Available bool      `json:"available"`
    CreatedAt time.Time `json:"created_at"`
}

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

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

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

type ValidationError struct {
    Field   سلسلة `json:"field"`
    Message سلسلة `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, خطأ) {
    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, خطأ) {
    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:"خطأ,omitempty"`
    Meta  *Meta       `json:"meta,omitempty"`
}

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

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

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

func respondError(w http.ResponseWriter, حالة int, msg سلسلة, details ...[]ValidationError) {
    err := APIError{Code: حالة, Message: msg}
    if len(details) > 0 {
        err.Details = details[0]
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(حالة)
    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 سلسلة) {
    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[سلسلة]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 خادم خطأ")
            }
        }()
        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[سلسلة]سلسلة{"حالة": "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
🔥 Common Mistake: Be careful when using interface{} as a استجابة type—the zero value of a map is serialized to فارغ in JSON, not {}. A best practice is to define an explicit استجابة struct (such as APIResponse{Data, Error, Meta}) to ensure that fields appear when they have values and are omitted when empty (omitempty).


❓ FAQ

Q How do you version a RESTful API?
A There are four ways: (1) URL path /api/v1/ (most common); (2) Request header Accept: application/vnd.api+json; version=1; (3) Query parameter ?v=1; (4) Subdomain v1.api.example.com. The URL path is recommended—it's the most intuitive and involves the lowest development and debugging costs.
Q How can we standardize خطأ formatting?
A Define an APIError struct that includes code, message, and details fields. All handlers should use the standardized respondError(w, حالة, msg) دالة. The front end only needs to check if resp.خطأ to display the خطأ, without worrying about the specific API.
Q Should parameter validation be performed at the handler or service layer?
A Basic validation (required fields, format) is performed at the handler layer, while business validation (uniqueness, permissions) is performed at the service layer. If validation fails at the handler layer, a 400 or 422 حالة code is returned; if it fails at the service layer, a 409 or 403 حالة code is returned.
Q What is the difference between PUT and PATCH?
A PUT is a full replacement—the عميل sends the complete resource, and any missing fields are treated as reset. PATCH is a partial update—the عميل sends only the fields to be modified. In terms of implementation, PUT is simpler, while PATCH requires handling the merging of partial fields. It is recommended to use PUT for CRUD operations and PATCH for complex updates.
Q How do I handle a 404 "no route found" خطأ?
A By default, ServeMux returns a 404 page. Customization: Create a catch-all handler: mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") }). Note that this handler should be registered last, because ServeMux uses best-match routing.
Q How is pagination implemented?
A Using the query parameters ?page=1&per_page=20. The Handler parses the parameters, and the Store layer performs the query with LIMIT and OFFSET. The استجابة returns meta: {total, page, per_page} for the واجهة أمامية to calculate the pagination component. The {path...} syntax in Go 1.22 routing is not suitable for pagination parameters—parameters should be in the query سلسلة.
Q Does HATEOAS (Hypermedia-As-a-Service) need to be implemented?
A No. HATEOAS is rarely actually used in REST API practice. Most APIs only need to return resource data and metadata. The front end knows what to do next from the API documentation; it doesn't need the API to tell it "what it can do."

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Build an Author REST API. Requirements: (1) Full CRUD functionality; (2) Use the /api/v1/authors path; (3) Standardized خطأ format; (4) Basic parameter validation (name is required). Test all endpoints using curl.

  2. Advanced Exercise (Difficulty ⭐⭐): Implement an Article API that associates articles with authors. Requirements: (1) POST /articles to create an article (associated with an existing author); (2) GET /articles?author_id=X to filter by author; (3) Support pagination (using the page and per_page parameters); (4) Use a uniform استجابة format: {data, meta}; (5) Validate that title and content are not empty.

  3. Challenge (Difficulty: ⭐⭐⭐): Implement a user management system with a complete وسيط chain. Requirements: (1) Routes: users (CRUD) + auth (login/registration); (2) وسيط: Recovery → Logging → CORS → RateLimit (token bucket) → Auth (Bearer Token) → Timeout; (3) Encrypt passwords using bcrypt during registration; return a JWT upon login; (4) The authentication وسيط should parse the userID from the JWT and inject it into the Context; (5) Use -race to verify concurrency safety.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏