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
- RESTful Design Principles (Resources/Verbs/Status Codes)
- Building a REST API with Go 1.22 Routes
- Request Parameter Validation
- Middleware Chain Integration
- Standardize the خطأ استجابة format
- API Version Management Policy
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!"
// 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
// 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
// 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
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)
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))
}
5. Parameter Validation
▶ Example: Structured Validation
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. Middleware Chain Integration
▶ Example: API وسيط chain
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. Complete Example: Library Management System 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 سلسلة `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))
}
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{} 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
/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.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.mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") }). Note that this handler should be registered last, because ServeMux uses best-match routing.?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 سلسلة.📖 Summary
- REST Design: Resource (noun) + Method (verb) + Status Code
- URL version management: The
/api/v1/approach is highly recommended - Standardize the خطأ format:
{خطأ: {code, message, details}} - Parameter Validation: Basic Validation (Handler) + Business Validation (Service)
- Middleware chain: Recovery → Logging → CORS → Auth → Timeout → Handler
- Unified استجابة functions:
respond()/respondError() - Pagination:
?page=N&per_page=N+ meta information
📝 Exercises
-
Basic Exercise (Difficulty ⭐): Build an Author REST API. Requirements: (1) Full CRUD functionality; (2) Use the
/api/v1/authorspath; (3) Standardized خطأ format; (4) Basic parameter validation (name is required). Test all endpoints using curl. -
Advanced Exercise (Difficulty ⭐⭐): Implement an Article API that associates articles with authors. Requirements: (1)
POST /articlesto create an article (associated with an existing author); (2)GET /articles?author_id=Xto filter by author; (3) Support pagination (using thepageandper_pageparameters); (4) Use a uniform استجابة format:{data, meta}; (5) Validate thattitleandcontentare not empty. -
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
-raceto verify concurrency safety.