REST API Development
Building a REST API in Go combines net/http routing, JSON encoding, validation, and layered architecture (handler → service → repository).
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.
sequenceDiagramClient->>Server: HTTP RequestServer->>Handler: ServeHTTPHandler->>Service: Business LogicService->>DB: QueryDB-->>Handler: DataHandler-->>Client: JSON Response
Step-by-step explanation
- Router maps method+path to handler function.
- Handler decodes JSON body into request DTO.
- Service validates business rules, calls repository.
- Repository persists to database or in-memory store.
- Handler encodes response DTO or error with correct status.
- Integration tests spin httptest.Server against handlers.
Practical code example
REST Product API with handler, service layer, and JSON error responses:
package mainimport ("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.RWMutexproducts 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 Productif err := json.NewDecoder(r.Body).Decode(&p); err != nil {writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})return}p.ID = idstore.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 viaMaxBytesReader.http.Error(w, msg, code)sends a plain-text error response quickly.switch r.Methoddispatches GET/POST/PATCH/DELETE within a single path pattern.middleware func(next http.Handler) http.Handlerwraps handlers for cross-cutting concerns.w.Header().Set("Content-Type", "application/json")sets response headers before WriteHeader.map domain errors: ErrNotFound → 404, ErrConflict → 409keeps 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?
Q2Beginner201 vs 200?
Q3IntermediateStructure Go REST project?
Q4IntermediateValidate request body?
Q5AdvancedDesign idempotent POST for payments.
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.