REST API Development

REST API Development

Real-World Analogy

Imagine you run a restaurant. Customers (clients) come to the front desk, and the waiter (router) directs them to different windows based on their needs:

The kitchen (server) processes the request and delivers the prepared dish (response) back to the customer through the waiter. Meanwhile, the security guard (middleware) checks identity before customers enter, records visit times, and can handle emergencies.

A REST API is a standardized service workflow like this — clients use uniform "verbs" (HTTP methods) and "addresses" (URLs) to communicate with the server, and the server returns structured data (JSON).

Project Requirements

We'll develop a Todo REST API supporting the following features:

Operation Method Path Description
Get all todos GET /api/todos Returns the todo list
Get single todo GET /api/todos/{id} Returns details by ID
Create todo POST /api/todos Adds a new todo
Update todo PUT /api/todos/{id} Modifies the specified todo
Delete todo DELETE /api/todos/{id} Deletes the specified todo

Additional requirements:

System Design

Client Request
    │
    ▼
┌──────────────────────────────┐
│         Middleware Chain      │
│  Recovery → Auth → Logging   │
│         ↓         ↓         ↓ │
└──────────────────────────────┘
    │
    ▼
┌──────────────────────────────┐
│          Router              │
│  GET    /api/todos           │
│  GET    /api/todos/{id}      │
│  POST   /api/todos           │
│  PUT    /api/todos/{id}      │
│  DELETE /api/todos/{id}      │
└──────────────────────────────┘
    │
    ▼
┌──────────────────────────────┐
│     In-Memory Storage Layer  │
│   map[string]Todo + sync.RWMutex │
└──────────────────────────────┘

Example 1: Complete Code

GO
package main

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

	"github.com/google/uuid"
)

// Todo item structure
type Todo struct {
	ID        string `json:"id"`
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

// TodoStore in-memory storage with read-write lock for concurrency safety
type TodoStore struct {
	mu    sync.RWMutex
	todos map[string]Todo
}

// NewTodoStore creates a new storage instance
func NewTodoStore() *TodoStore {
	return &TodoStore{
		todos: make(map[string]Todo),
	}
}

// GetAll returns all todo items
func (s *TodoStore) GetAll() []Todo {
	s.mu.RLock()
	defer s.mu.RUnlock()

	list := make([]Todo, 0, len(s.todos))
	for _, t := range s.todos {
		list = append(list, t)
	}
	return list
}

// GetByID returns a single todo item by ID
func (s *TodoStore) GetByID(id string) (Todo, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	t, ok := s.todos[id]
	return t, ok
}

// Create creates a new todo item
func (s *TodoStore) Create(title string) Todo {
	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now().Format(time.RFC3339)
	t := Todo{
		ID:        uuid.New().String(),
		Title:     title,
		Completed: false,
		CreatedAt: now,
		UpdatedAt: now,
	}
	s.todos[t.ID] = t
	return t
}

// Update updates the specified todo item
func (s *TodoStore) Update(id string, title string, completed bool) (Todo, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()

	t, ok := s.todos[id]
	if !ok {
		return Todo{}, false
	}

	t.Title = title
	t.Completed = completed
	t.UpdatedAt = time.Now().Format(time.RFC3339)
	s.todos[id] = t
	return t, true
}

// Delete deletes the specified todo item
func (s *TodoStore) Delete(id string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	if _, ok := s.todos[id]; !ok {
		return false
	}
	delete(s.todos, id)
	return true
}

// --- Common Response Structures ---

// APIResponse unified API response format
type APIResponse struct {
	Success bool        `json:"success"`
	Data    interface{} `json:"data,omitempty"`
	Error   string      `json:"error,omitempty"`
}

// jsonResponse sends a JSON response
func jsonResponse(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

// --- Middleware ---

// Middleware type definition
type Middleware func(http.Handler) http.Handler

// RecoveryMiddleware panic recovery middleware
// Catches panics in handler functions, returns 500 error instead of crashing the process
func RecoveryMiddleware(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)
				jsonResponse(w, http.StatusInternalServerError, APIResponse{
					Success: false,
					Error:   "Internal server error",
				})
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// LoggingMiddleware request logging middleware
// Records each request's method, path, and duration
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("[LOG] %s %s %s", r.Method, r.URL.Path, time.Since(start))
	})
}

// AuthMiddleware simple authentication middleware
// Checks whether the request header contains a valid API Key
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		apiKey := r.Header.Get("X-API-Key")
		if apiKey != "secret-key-123" {
			jsonResponse(w, http.StatusUnauthorized, APIResponse{
				Success: false,
				Error:   "Invalid API key",
			})
			return
		}
		next.ServeHTTP(w, r)
	})
}

// Chain combines multiple middleware into a chain
// Parameter order is execution order: Chain(A, B, C) → A(B(C(handler)))
func Chain(middlewares ...Middleware) Middleware {
	return func(next http.Handler) http.Handler {
		for i := len(middlewares) - 1; i >= 0; i-- {
			next = middlewares[i](next)
		}
		return next
	}
}

// --- Router and Handlers ---

// Router custom router
type Router struct {
	routes     map[string]map[string]http.HandlerFunc // method -> path -> handler
	middleware Middleware
}

// NewRouter creates a router
func NewRouter() *Router {
	return &Router{
		routes: make(map[string]map[string]http.HandlerFunc),
	}
}

// Use registers global middleware
func (rt *Router) Use(m Middleware) {
	rt.middleware = m
}

// Handle registers a route: method + path + handler
func (rt *Router) Handle(method, path string, handler http.HandlerFunc) {
	if _, ok := rt.routes[method]; !ok {
		rt.routes[method] = make(map[string]http.HandlerFunc)
	}
	rt.routes[method][path] = handler
}

// ServeHTTP implements the http.Handler interface, completing route matching
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	methodRoutes, ok := rt.routes[r.Method]
	if !ok {
		jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{
			Success: false,
			Error:   "Method not allowed",
		})
		return
	}

	// Exact match
	if handler, ok := methodRoutes[r.URL.Path]; ok {
		handler(w, r)
		return
	}

	// Prefix match: try to extract path parameters (e.g., /api/todos/{id})
	for pattern, handler := range methodRoutes {
		if strings.HasSuffix(pattern, "/{id}") {
			prefix := strings.TrimSuffix(pattern, "/{id}")
			if strings.HasPrefix(r.URL.Path, prefix+"/") {
				// Put {id} into request header for the handler to read
				id := strings.TrimPrefix(r.URL.Path, prefix+"/")
				r.Header.Set("X-Resource-ID", id)
				handler(w, r)
				return
			}
		}
	}

	jsonResponse(w, http.StatusNotFound, APIResponse{
		Success: false,
		Error:   "Resource not found",
	})
}

// --- Handler Functions ---

// handleGetTodos returns all todo items
func handleGetTodos(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		todos := store.GetAll()
		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    todos,
		})
	}
}

// handleGetTodo returns a single todo item
func handleGetTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Resource-ID")
		todo, ok := store.GetByID(id)
		if !ok {
			jsonResponse(w, http.StatusNotFound, APIResponse{
				Success: false,
				Error:   "Todo item not found",
			})
			return
		}
		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    todo,
		})
	}
}

// CreateTodoRequest request body for creating a todo
type CreateTodoRequest struct {
	Title string `json:"title"`
}

// handleCreateTodo creates a todo item
func handleCreateTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req CreateTodoRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "Invalid request body",
			})
			return
		}

		if strings.TrimSpace(req.Title) == "" {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "Title cannot be empty",
			})
			return
		}

		todo := store.Create(req.Title)
		jsonResponse(w, http.StatusCreated, APIResponse{
			Success: true,
			Data:    todo,
		})
	}
}

// UpdateTodoRequest request body for updating a todo
type UpdateTodoRequest struct {
	Title     string `json:"title"`
	Completed bool   `json:"completed"`
}

// handleUpdateTodo updates a todo item
func handleUpdateTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Resource-ID")

		var req UpdateTodoRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "Invalid request body",
			})
			return
		}

		if strings.TrimSpace(req.Title) == "" {
			jsonResponse(w, http.StatusBadRequest, APIResponse{
				Success: false,
				Error:   "Title cannot be empty",
			})
			return
		}

		todo, ok := store.Update(id, req.Title, req.Completed)
		if !ok {
			jsonResponse(w, http.StatusNotFound, APIResponse{
				Success: false,
				Error:   "Todo item not found",
			})
			return
		}

		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    todo,
		})
	}
}

// handleDeleteTodo deletes a todo item
func handleDeleteTodo(store *TodoStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := r.Header.Get("X-Resource-ID")
		if !store.Delete(id) {
			jsonResponse(w, http.StatusNotFound, APIResponse{
				Success: false,
				Error:   "Todo item not found",
			})
			return
		}
		jsonResponse(w, http.StatusOK, APIResponse{
			Success: true,
			Data:    "Deleted successfully",
		})
	}
}

func main() {
	// Initialize storage
	store := NewTodoStore()

	// Create router
	router := NewRouter()

	// Register middleware chain: Recovery → Auth → Logging
	router.Use(Chain(RecoveryMiddleware, AuthMiddleware, LoggingMiddleware))

	// Register routes
	router.Handle("GET", "/api/todos", handleGetTodos(store))
	router.Handle("GET", "/api/todos/{id}", handleGetTodo(store))
	router.Handle("POST", "/api/todos", handleCreateTodo(store))
	router.Handle("PUT", "/api/todos/{id}", handleUpdateTodo(store))
	router.Handle("DELETE", "/api/todos/{id}", handleDeleteTodo(store))

	// Apply middleware and start server
	addr := ":8080"
	log.Printf("REST API server started at http://localhost%s", addr)
	log.Fatal(http.ListenAndServe(addr, router.middleware(router)))
}
▶ Try it Yourself

Running and Testing

Before starting the service, initialize the module and install dependencies:

BASH
go mod init todo-api
go get github.com/google/uuid
go run main.go

Use curl to test each endpoint:

BASH
# Create a todo item
curl -X POST http://localhost:8080/api/todos \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret-key-123" \
  -d '{"title": "Learn Go"}'

# Get all todos
curl http://localhost:8080/api/todos \
  -H "X-API-Key: secret-key-123"

# Get a single todo (replace {id} with the actual returned ID)
curl http://localhost:8080/api/todos/{id} \
  -H "X-API-Key: secret-key-123"

# Update a todo
curl -X PUT http://localhost:8080/api/todos/{id} \
  -H "Content-Type: application/json" \
  -H "X-API-Key: secret-key-123" \
  -d '{"title": "Learn Go (completed)", "completed": true}'

# Delete a todo
curl -X DELETE http://localhost:8080/api/todos/{id} \
  -H "X-API-Key: secret-key-123"

# Test unauthenticated request (should return 401)
curl http://localhost:8080/api/todos

Example expected responses:

TEXT
# Successful creation
{"success":true,"data":{"id":"a1b2c3d4-...","title":"Learn Go","completed":false,"created_at":"2026-06-27T10:00:00+08:00","updated_at":"2026-06-27T10:00:00+08:00"}}

# Unauthenticated
{"success":false,"error":"Invalid API key"}

Code Analysis

1. Data Model and Storage Layer

The Todo struct defines the data structure for todo items, using JSON tags to control serialized field names. TodoStore uses sync.RWMutex to protect the map — read operations use a read lock (RLock), write operations use a write lock (Lock), ensuring concurrency safety.

2. Middleware Pattern

Each middleware is of type func(http.Handler) http.Handler, wrapping the next handler through the decorator pattern:

TEXT
RecoveryMiddleware(
  AuthMiddleware(
    LoggingMiddleware(
      final handler,
    ),
  ),
)

The Chain function combines multiple middleware in parameter order into a chain.

3. Custom Router

Since the standard library's http.ServeMux didn't support path parameters before Go 1.22, we implemented a simple router:

4. Handler Closures

Functions like handleGetTodos(store) return a closure that captures the store reference. This allows handlers to access the shared storage layer without global variables.

5. Unified Response Format

All endpoints return the same JSON structure {success, data, error}, so clients only need to parse once to determine if the request was successful.

❓ FAQ

Q1: Why use sync.RWMutex instead of sync.Mutex?

Mutex only allows one goroutine to access at a time, while RWMutex allows multiple read operations to execute concurrently — only write operations exclusively lock. For "read-heavy, write-light" scenarios (like API queries far outnumbering modifications), RWMutex can significantly improve concurrent performance.

Q2: Should this router be used in production?

Not recommended. This router is a simplified implementation for educational purposes, lacking support for regex matching, wildcards, method auto-detection, and other advanced features. For production, use mature routing libraries like chi, gorilla/mux, or Go 1.22+'s built-in enhanced routing.

Q3: In-memory storage data is lost after restart. How to solve this?

This lesson uses in-memory storage to focus on the REST API part. In real projects, you should persist data to a database (such as SQLite, PostgreSQL, MongoDB). The next lesson will cover how to operate databases with Go.

Q4: Does the middleware execution order matter?

Yes. Recovery goes outermost to ensure it catches all inner panics; Auth goes before Logging so unauthenticated requests don't generate business logs (though you could do the opposite to log all requests including unauthenticated ones). Adjust flexibly based on business needs.

📖 Summary

In this lesson we comprehensively applied multiple knowledge points learned earlier to build a complete REST API from scratch:

These patterns are the foundation of Go web development. After mastering them, you can easily extend to database integration, JWT authentication, API rate limiting, and other complex scenarios.

📝 Exercises

Exercise 1: Add Pagination Support

Modify the GET /api/todos endpoint to support query parameters ?page=1&size=10 for paginated results. Hint: use r.URL.Query() to read parameters and slice the GetAll results.

Exercise 2: Add CORS Middleware

Write CORSMiddleware that adds the following to response headers:

Handle OPTIONS preflight requests by returning 204 directly. Add it to the middleware chain.

Exercise 3: Implement Search Functionality

Add a new route GET /api/todos/search?q=keyword that performs fuzzy title matching in the in-memory storage (using strings.Contains) and returns matching todo items.


Next Lesson: Database Operations

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%

🙏 帮我们做得更好

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

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