Go: تطبيق واجهة برمجة التطبيقات REST عمليًّا

واجهة برمجة التطبيقات (REST API) لا تقتصر على عمليات CRUD فحسب — بل تشمل تصميم الموارد، واختيار رموز الحالة، وتوحيد تنسيق الأخطاء، وسلاسل البرامج الوسيطة: فكل تفصيل من هذه التفاصيل يحدد جودة واجهة برمجة التطبيقات على مستوى الإنتاج.

عندما يتعين استدعاء واجهة برمجة التطبيقات (API) الخاصة بك من قِبل كل من تطبيقات الواجهة الأمامية وخدمات الجهات الخارجية، فإن وجود تنسيق موحد للأخطاء، ورموز حالة مناسبة، وإدارة واضحة للإصدارات لم يعد مجرد «ميزات إضافية» — بل أصبح «متطلبات أساسية».

1. ستتعلم



2. قصة حقيقية من أحد المتعاونين في مجال الواجهة الأمامية

(1) المشكلة: تختلف تنسيقات أخطاء واجهة برمجة التطبيقات (API) باختلاف الواجهة، مما يتسبب في تعطل الواجهة الأمامية

تتعاون فرق «الخلفية» و«الواجهة» في شركة «أليس» على مشروع للتجارة الإلكترونية:

«قال زملائي في قسم الواجهة الأمامية: "تنسيقات الأخطاء تختلف من واجهة برمجة تطبيقات (API) إلى أخرى. فقائمة المستخدمين تُرجع {"error":"not found"}، وواجهة برمجة تطبيقات الطلبات تُرجع {"message":"Order not found","code":404}، أما واجهة برمجة تطبيقات المنتجات فتُرجع صفحة خطأ 500 فحسب. وأنا مضطر لكتابة كود مختلف لمعالجة الأخطاء لكل واجهة برمجة تطبيقات!"»

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) الإيرادات: قبل التوحيد وبعده

البعد غير متسق التنسيق الموحد
المعالجة الأمامية كتابة منطق مختلف لكل واجهة برمجة تطبيقات if (resp.error) handleError(resp)
تكلفة التوثيق توثيق منفصل لكل واجهة برمجة تطبيقات تنسيق الوصف المكون من جملة واحدة
إنشاء 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 تم الإنشاء نشر تم الإنشاء بنجاح
204 لا يوجد محتوى حذف تم الحذف بنجاح
400 طلب غير صحيح معلمات طلب غير صالحة
401 غير مصرح به غير مصرح به
403 ممنوع لا يوجد إذن
404 غير موجود المورد غير موجود
409 تعارض POST/PUT تعارض في الموارد (مثل إنشاء نسخة مكررة)
422 غير قابل للمعالجة POST/PUT خطأ دلالي في نص الطلب
429 عدد زائد حد عدد الطلبات
500 خطأ داخلي خطأ في الخادم


4. تجربة عملية مع واجهة برمجة التطبيقات REST

▶ مثال: واجهة برمجة تطبيقات CRUD للمستخدم

⚙️ المتطلبات المسبقة: قم بتشغيل 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. مثال كامل: واجهة برمجة تطبيقات نظام إدارة المكتبات

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{} كنوع استجابة — حيث يتم تسلسل القيمة الصفرية للخريطة إلى null في JSON، وليس {}. من أفضل الممارسات تعريف بنية استجابة صريحة (مثل APIResponse{Data, Error, Meta}) لضمان ظهور الحقول عندما تحتوي على قيم وحذفها عندما تكون فارغة (omitempty).



❓ أسئلة شائعة

س كيف يتم تحديد إصدار واجهة برمجة تطبيقات RESTful؟
ج هناك أربع طرق: (1) مسار عنوان URL /api/v1/ (الأكثر شيوعًا)؛ (2) رأس الطلب Accept: application/vnd.api+json; version=1؛ (3) معلمة الاستعلام ?v=1؛ (4) النطاق الفرعي v1.api.example.com. يُوصى باستخدام مسار URL — فهو الأكثر بديهية ويتطلب أقل تكاليف للتطوير وتصحيح الأخطاء.
س كيف يمكننا توحيد تنسيق عرض الأخطاء؟
ج قم بتعريف بنية APIError تتضمن حقولًا للرمز والرسالة والتفاصيل. يجب على جميع معالجات الأخطاء استخدام الدالة الموحدة respondError(w, status, msg). ولا يتعين على الواجهة الأمامية سوى التحقق من if resp.error لعرض الخطأ، دون الحاجة إلى الاهتمام بواجهة برمجة التطبيقات (API) المحددة.
س هل يجب إجراء التحقق من صحة المعلمات في طبقة المعالج أم في طبقة الخدمة؟
ج يتم إجراء التحقق الأساسي من الصحة (الحقول الإلزامية، التنسيق) في طبقة المعالج، بينما يتم إجراء التحقق من الصحة المتعلقة بالأعمال (التفرد، الأذونات) في طبقة الخدمة. إذا فشل التحقق من الصحة في طبقة المعالج، يتم إرجاع رمز الحالة 400 أو 422؛ وإذا فشل في طبقة الخدمة، يتم إرجاع رمز الحالة 409 أو 403.
س ما الفرق بين PUT و PATCH؟
ج PUT هي عملية استبدال كاملة — حيث يرسل العميل المورد بالكامل، ويتم التعامل مع أي حقول مفقودة على أنها أعيد تعيينها. أما PATCH فهي عملية تحديث جزئي — حيث يرسل العميل الحقول المراد تعديلها فقط. من حيث التنفيذ، تعتبر PUT أبسط، بينما تتطلب PATCH معالجة دمج الحقول الجزئية. يُنصح باستخدام PUT لعمليات CRUD وPATCH للتحديثات المعقدة.
س كيف أتعامل مع خطأ 404 «لم يتم العثور على المسار»؟
ج بشكل افتراضي، يعرض ServeMux صفحة خطأ 404. التخصيص: قم بإنشاء معالج شامل: mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") }). لاحظ أنه يجب تسجيل هذا المعالج في النهاية، لأن ServeMux يستخدم التوجيه القائم على أفضل تطابق.
س كيف يتم تنفيذ ترقيم الصفحات؟
ج باستخدام معلمات الاستعلام ?page=1&per_page=20. يقوم المعالج (Handler) بتحليل المعلمات، وتقوم طبقة التخزين (Store) بتنفيذ الاستعلام باستخدام LIMIT و OFFSET. وتُرجع الاستجابة meta: {total, page, per_page} إلى الواجهة الأمامية لحساب مكون ترقيم الصفحات. لا تناسب صيغة {path...} في توجيه Go 1.22 معلمات ترقيم الصفحات — يجب أن تكون المعلمات في سلسلة الاستعلام.
س هل يلزم تطبيق HATEOAS (Hypermedia-As-a-Service)؟
ج لا. نادرًا ما يُستخدم HATEOAS فعليًّا في الممارسة العملية لواجهة برمجة التطبيقات REST. فمعظم واجهات برمجة التطبيقات تحتاج فقط إلى إرجاع بيانات الموارد والبيانات الوصفية. وتعرف الواجهة الأمامية ما يجب عليها فعله بعد ذلك من خلال وثائق واجهة برمجة التطبيقات؛ ولا تحتاج إلى أن تخبرها واجهة برمجة التطبيقات «بما يمكنها فعله».

📖 ملخص


📝 تمارين

  1. تمرين أساسي (مستوى الصعوبة ⭐): قم بإنشاء واجهة برمجة تطبيقات REST خاصة بالمؤلف. المتطلبات: (1) وظائف CRUD كاملة؛ (2) استخدام المسار /api/v1/authors؛ (3) تنسيق أخطاء موحد؛ (4) التحقق الأساسي من صحة المعلمات (الاسم مطلوب). اختبر جميع نقاط النهاية باستخدام curl.

  2. تمرين متقدم (مستوى الصعوبة ⭐⭐): قم بتنفيذ واجهة برمجة تطبيقات (API) للمقالات تربط المقالات بالمؤلفين. المتطلبات: (1) POST /articles لإنشاء مقال (مرتبط بمؤلف موجود)؛ (2) GET /articles?author_id=X للتصفية حسب المؤلف؛ (3) دعم ترقيم الصفحات (باستخدام المعلمتين page وper_page)؛ (4) استخدام تنسيق استجابة موحد: {data, meta}؛ (5) التحقق من أن title وcontent غير فارغتين.

  3. التحدي (الصعوبة: ⭐⭐⭐): تنفيذ نظام لإدارة المستخدمين مع سلسلة كاملة من البرمجيات الوسيطة. المتطلبات: (1) المسارات: المستخدمون (CRUD) + المصادقة (تسجيل الدخول/التسجيل)؛ (2) البرامج الوسيطة: الاسترداد → التسجيل → CORS → RateLimit (token bucket) → المصادقة (Bearer Token) → انتهاء المهلة؛ (3) تشفير كلمات المرور باستخدام bcrypt أثناء التسجيل؛ إرجاع JWT عند تسجيل الدخول؛ (4) يجب أن تقوم البرمجيات الوسيطة للمصادقة بتحليل معرّف المستخدم من JWT وإدراجه في السياق؛ (5) استخدم -race للتحقق من أمان التزامن.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%