1. go
  2. /web
  3. /rest-apis

Building REST APIs in Go

Building RESTful APIs in Go involves creating endpoints that follow REST principles. This guide covers how to build robust and scalable REST APIs.

Basic API Structure

API Router Setup

type API struct {
    router *mux.Router
    db     *sql.DB
}

func NewAPI(db *sql.DB) *API {
    api := &API{
        router: mux.NewRouter(),
        db:     db,
    }
    
    api.routes()
    return api
}

func (a *API) routes() {
    // API versioning
    v1 := a.router.PathPrefix("/api/v1").Subrouter()
    
    // Routes
    v1.HandleFunc("/users", a.listUsers).Methods("GET")
    v1.HandleFunc("/users", a.createUser).Methods("POST")
    v1.HandleFunc("/users/{id}", a.getUser).Methods("GET")
    v1.HandleFunc("/users/{id}", a.updateUser).Methods("PUT")
    v1.HandleFunc("/users/{id}", a.deleteUser).Methods("DELETE")
}

Request Handling

type UserHandler struct {
    service *UserService
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    // Get URL parameters
    vars := mux.Vars(r)
    id := vars["id"]
    
    // Get query parameters
    query := r.URL.Query()
    fields := query.Get("fields")
    
    // Get user from service
    user, err := h.service.GetUser(id, fields)
    if err != nil {
        if err == sql.ErrNoRows {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }
        http.Error(w, "Server error", http.StatusInternalServerError)
        return
    }
    
    // Return response
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

Request/Response Handling

Request Validation

type CreateUserRequest struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

func (h *UserHandler) createUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    
    // Parse request body
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // Validate request
    validate := validator.New()
    if err := validate.Struct(req); err != nil {
        errors := make(map[string]string)
        for _, err := range err.(validator.ValidationErrors) {
            errors[err.Field()] = err.Tag()
        }
        
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(map[string]interface{}{
            "errors": errors,
        })
        return
    }
    
    // Process request
    user, err := h.service.CreateUser(req)
    if err != nil {
        http.Error(w, "Server error", http.StatusInternalServerError)
        return
    }
    
    // Return response
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

Response Formatting

type Response struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Error   *Error     `json:"error,omitempty"`
}

type Error struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

func WriteJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    
    response := Response{
        Success: status >= 200 && status < 300,
        Data:    data,
    }
    
    if err := json.NewEncoder(w).Encode(response); err != nil {
        log.Printf("Error encoding response: %v", err)
    }
}

func WriteError(w http.ResponseWriter, status int, code, message string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    
    response := Response{
        Success: false,
        Error: &Error{
            Code:    code,
            Message: message,
        },
    }
    
    if err := json.NewEncoder(w).Encode(response); err != nil {
        log.Printf("Error encoding error response: %v", err)
    }
}

API Middleware

Request Logging

type ResponseRecorder struct {
    http.ResponseWriter
    status int
    size   int64
}

func (r *ResponseRecorder) WriteHeader(status int) {
    r.status = status
    r.ResponseWriter.WriteHeader(status)
}

func (r *ResponseRecorder) Write(b []byte) (int, error) {
    size, err := r.ResponseWriter.Write(b)
    r.size += int64(size)
    return size, err
}

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        recorder := &ResponseRecorder{
            ResponseWriter: w,
            status:        http.StatusOK,
        }
        
        next.ServeHTTP(recorder, r)
        
        duration := time.Since(start)
        
        log.Printf(
            "%s %s %d %d %s",
            r.Method,
            r.URL.Path,
            recorder.status,
            recorder.size,
            duration,
        )
    })
}

Rate Limiting

type RateLimiter struct {
    store  *redis.Client
    limit  int
    window time.Duration
}

func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Get client identifier (IP or API key)
        key := fmt.Sprintf("rate:%s", r.RemoteAddr)
        
        // Get current count
        count, err := rl.store.Get(context.Background(), key).Int()
        if err != nil && err != redis.Nil {
            http.Error(w, "Server error", http.StatusInternalServerError)
            return
        }
        
        if count >= rl.limit {
            w.Header().Set("X-RateLimit-Limit", strconv.Itoa(rl.limit))
            w.Header().Set("X-RateLimit-Remaining", "0")
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        
        // Increment count
        pipe := rl.store.Pipeline()
        pipe.Incr(context.Background(), key)
        pipe.Expire(context.Background(), key, rl.window)
        if _, err := pipe.Exec(context.Background()); err != nil {
            http.Error(w, "Server error", http.StatusInternalServerError)
            return
        }
        
        w.Header().Set("X-RateLimit-Limit", strconv.Itoa(rl.limit))
        w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(rl.limit-count-1))
        
        next.ServeHTTP(w, r)
    })
}

Best Practices

1. Resource Organization

type Resource struct {
    router  *mux.Router
    service Service
}

func (r *Resource) Register(path string) {
    // Base routes
    r.router.HandleFunc(path, r.List).Methods("GET")
    r.router.HandleFunc(path, r.Create).Methods("POST")
    
    // Single item routes
    itemPath := path + "/{id}"
    r.router.HandleFunc(itemPath, r.Get).Methods("GET")
    r.router.HandleFunc(itemPath, r.Update).Methods("PUT")
    r.router.HandleFunc(itemPath, r.Delete).Methods("DELETE")
}

func (r *Resource) List(w http.ResponseWriter, req *http.Request) {
    // Get query parameters
    query := req.URL.Query()
    limit := query.Get("limit")
    offset := query.Get("offset")
    sort := query.Get("sort")
    
    // Get items
    items, err := r.service.List(ListParams{
        Limit:  limit,
        Offset: offset,
        Sort:   sort,
    })
    if err != nil {
        WriteError(w, http.StatusInternalServerError, "internal_error", err.Error())
        return
    }
    
    WriteJSON(w, http.StatusOK, items)
}

2. Error Handling

type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Details string `json:"details,omitempty"`
}

func (e *APIError) Error() string {
    return e.Message
}

func HandleError(err error, w http.ResponseWriter) {
    switch e := err.(type) {
    case *APIError:
        WriteError(w, http.StatusBadRequest, e.Code, e.Message)
    case *ValidationError:
        WriteError(w, http.StatusBadRequest, "validation_error", e.Error())
    case *NotFoundError:
        WriteError(w, http.StatusNotFound, "not_found", e.Error())
    default:
        WriteError(w, http.StatusInternalServerError, "internal_error", "Internal server error")
    }
}

3. Versioning

type APIVersion struct {
    Major int
    Minor int
}

func (v *APIVersion) String() string {
    return fmt.Sprintf("v%d.%d", v.Major, v.Minor)
}

func VersionMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        version := r.Header.Get("Accept-Version")
        if version == "" {
            version = "1.0" // Default version
        }
        
        // Parse version
        major, minor := parseVersion(version)
        v := &APIVersion{Major: major, Minor: minor}
        
        // Add version to context
        ctx := context.WithValue(r.Context(), "version", v)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Common Patterns

1. Pagination

type PaginationParams struct {
    Page     int    `json:"page"`
    PageSize int    `json:"page_size"`
    Sort     string `json:"sort"`
}

type PaginatedResponse struct {
    Items      interface{} `json:"items"`
    TotalItems int64      `json:"total_items"`
    Page       int        `json:"page"`
    PageSize   int        `json:"page_size"`
    TotalPages int        `json:"total_pages"`
}

func (h *Handler) ListWithPagination(w http.ResponseWriter, r *http.Request) {
    // Parse pagination parameters
    params := PaginationParams{
        Page:     1,
        PageSize: 10,
    }
    
    if page := r.URL.Query().Get("page"); page != "" {
        params.Page, _ = strconv.Atoi(page)
    }
    if size := r.URL.Query().Get("page_size"); size != "" {
        params.PageSize, _ = strconv.Atoi(size)
    }
    
    // Get items
    items, total, err := h.service.List(params)
    if err != nil {
        HandleError(err, w)
        return
    }
    
    // Calculate pagination info
    totalPages := (total + int64(params.PageSize) - 1) / int64(params.PageSize)
    
    response := PaginatedResponse{
        Items:      items,
        TotalItems: total,
        Page:       params.Page,
        PageSize:   params.PageSize,
        TotalPages: int(totalPages),
    }
    
    WriteJSON(w, http.StatusOK, response)
}
type FilterParams struct {
    Query  string            `json:"query"`
    Fields map[string]string `json:"fields"`
}

func (h *Handler) ListWithFilters(w http.ResponseWriter, r *http.Request) {
    // Parse filter parameters
    filters := FilterParams{
        Fields: make(map[string]string),
    }
    
    query := r.URL.Query()
    filters.Query = query.Get("q")
    
    // Parse field filters
    for key, values := range query {
        if strings.HasPrefix(key, "filter.") {
            field := strings.TrimPrefix(key, "filter.")
            filters.Fields[field] = values[0]
        }
    }
    
    // Apply filters
    items, err := h.service.Search(filters)
    if err != nil {
        HandleError(err, w)
        return
    }
    
    WriteJSON(w, http.StatusOK, items)
}

Next Steps