Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 32

    REST API Development

    Building a REST API in Go combines net/http routing, JSON encoding, validation, and layered architecture (handler → service → repository).

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Building a REST API in Go combines net/http routing, JSON encoding, validation, and layered architecture (handler → service → repository). Go 1.22's enhanced ServeMux simplifies routing without external frameworks, though chi, gin, and echo remain popular for larger projects.

    REST conventions — proper HTTP methods, status codes, resource naming, and error response format — matter as much as syntax. Interviewers ask you to design CRUD endpoints, handle validation errors as 400, and return consistent JSON error envelopes.

    This lesson implements a complete Product API with create, read, update, delete, and structured error responses using only the standard library plus idiomatic patterns.

    The story

    An internal API gateway at a fintech company exposes CRUD endpoints for merchant accounts: GET /merchants/{id}, POST /merchants, PATCH /merchants/{id}. Handlers parse path parameters, validate JSON bodies, map domain errors to HTTP status codes, and return consistent error envelopes — the contract frontends and B2B integrators depend on.

    REST in Go typically layers chi, gorilla/mux, or stdlib ServeMux (1.22+) with middleware for auth, request IDs, and Prometheus metrics.

    Understanding the topic

    Key concepts

    • Resources as nouns: /products, /products/{id} — verbs via HTTP methods.
    • GET read, POST create, PUT/PATCH update, DELETE remove.
    • Status: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal.
    • Handler parses request → calls service → writes JSON response.
    • Consistent error JSON: {"error":"message","code":"VALIDATION"}.
    • Middleware chain: recovery, logging, auth, CORS.
    text
    sequenceDiagram
    Client->>Server: HTTP Request
    Server->>Handler: ServeHTTP
    Handler->>Service: Business Logic
    Service->>DB: Query
    DB-->>Handler: Data
    Handler-->>Client: JSON Response

    Step-by-step explanation

    1. Router maps method+path to handler function.
    2. Handler decodes JSON body into request DTO.
    3. Service validates business rules, calls repository.
    4. Repository persists to database or in-memory store.
    5. Handler encodes response DTO or error with correct status.
    6. Integration tests spin httptest.Server against handlers.

    Practical code example

    REST Product API with handler, service layer, and JSON error responses:

    go
    package main
    import (
    "encoding/json"
    "errors"
    "net/http"
    "sync"
    )
    type Product struct {
    ID string `json:"id"`
    Name string `json:"name"`
    Price float64 `json:"price"`
    }
    type ProductStore struct {
    mu sync.RWMutex
    products map[string]Product
    }
    func (s *ProductStore) Get(id string) (Product, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    p, ok := s.products[id]
    if !ok {
    return Product{}, errors.New("not found")
    }
    return p, nil
    }
    func (s *ProductStore) Save(p Product) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.products[p.ID] = p
    }
    func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    _ = json.NewEncoder(w).Encode(v)
    }
    func productHandler(store *ProductStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    switch r.Method {
    case http.MethodGet:
    p, err := store.Get(id)
    if err != nil {
    writeJSON(w, http.StatusNotFound, map[string]string{"error": "product not found"})
    return
    }
    writeJSON(w, http.StatusOK, p)
    case http.MethodPut:
    var p Product
    if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
    writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
    return
    }
    p.ID = id
    store.Save(p)
    writeJSON(w, http.StatusOK, p)
    default:
    writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
    }
    }
    }
    func main() {
    store := &ProductStore{products: make(map[string]Product)}
    mux := http.NewServeMux()
    mux.HandleFunc("GET /products/{id}", productHandler(store))
    mux.HandleFunc("PUT /products/{id}", productHandler(store))
    http.ListenAndServe(":8080", mux)
    }

    Line-by-line code explanation

    • r.PathValue("id") (Go 1.22+ mux) extracts URL path parameters.
    • json.NewDecoder(r.Body).Decode(&req) parses JSON request bodies with size limits via MaxBytesReader.
    • http.Error(w, msg, code) sends a plain-text error response quickly.
    • switch r.Method dispatches GET/POST/PATCH/DELETE within a single path pattern.
    • middleware func(next http.Handler) http.Handler wraps handlers for cross-cutting concerns.
    • w.Header().Set("Content-Type", "application/json") sets response headers before WriteHeader.
    • map domain errors: ErrNotFound → 404, ErrConflict → 409 keeps HTTP mapping centralized.
    • httptest.NewRecorder() tests handlers without starting a real network listener.

    Key takeaway: Go 1.22 r.PathValue extracts {id}. Layer store behind interface for testing. Consistent writeJSON helper.

    Real-world use

    Where you'll use this in production

    • E-commerce product catalog and order APIs.
    • SaaS multi-tenant REST backends.
    • Mobile app BFF (backend-for-frontend) services.
    • Public developer APIs with rate limiting and API keys.

    Best practices

    • Use correct HTTP method and status for each operation.
    • Validate input; return 400 with clear error message.
    • Separate handler, service, repository layers.
    • Version APIs: /v1/products for backward compatibility.
    • Document with OpenAPI/Swagger for consumers.
    • Use httptest for handler integration tests.

    Common mistakes

    • 200 for everything including errors.
    • Business logic directly in handler — untestable.
    • Inconsistent error JSON shape across endpoints.
    • No request body size limit — DoS via large payload.
    • PUT vs PATCH confusion — PUT replaces, PATCH partial update.

    Advanced interview questions

    Q1BeginnerREST vs RPC?
    REST resource-oriented with HTTP verbs; RPC action-oriented (gRPC POST /doThing).
    Q2Beginner201 vs 200?
    201 Created for new resource with Location header; 200 for successful read/update.
    Q3IntermediateStructure Go REST project?
    cmd/api/main, internal/handler, internal/service, internal/repo, internal/domain.
    Q4IntermediateValidate request body?
    Decode JSON, run validator (go-playground/validator), return 400 with field errors.
    Q5AdvancedDesign idempotent POST for payments.
    Idempotency-Key header; store key→response; return cached response on duplicate.

    Summary

    REST maps resources to URLs and HTTP methods. Layer handlers, services, and repositories for testability. Return correct status codes and consistent JSON errors. Go 1.22 ServeMux simplifies routing with path parameters. Next lesson: PostgreSQL/MySQL with database/sql.

    Ready to mark this lesson complete?Track your journey across the entire course.