Best examples of REST API with Go: Practical Examples in Go

If you learn best by seeing real code, you’re in the right place. This guide focuses on **examples of REST API with Go: practical examples** that you can copy, tweak, and ship. Instead of abstract theory, we’ll walk through realistic patterns you’ll actually use in production. We’ll start with a tiny in-memory API, then move into JSON handling, routing, middleware, authentication, database integration, and versioning. Along the way, you’ll see how each example of REST API with Go fits into a modern backend stack in 2024–2025, from simple microservices to cloud-native deployments. The goal isn’t to impress you with clever tricks. It’s to give you a set of **real examples** you can paste into your editor and adapt for your own services. If you’ve written some Go but feel shaky about HTTP handlers, routing, or structuring an API, these practical examples will close that gap fast.
Written by
Jamie
Published

Let’s start with the smallest example of a REST API in Go that still feels realistic: an in-memory “todo” service using only the standard library.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
)

type Todo struct {
    ID      int    `json:"id"`
    Title   string `json:"title"`
    Done    bool   `json:"done"`
}

var todos = []Todo{
    {ID: 1, Title: "Learn Go", Done: false},
    {ID: 2, Title: "Build REST API", Done: false},
}

func main() {
    http.HandleFunc("/todos", handleTodos)
    http.HandleFunc("/todos/", handleTodoByID)

    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleTodos(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {
    case http.MethodGet:
        json.NewEncoder(w).Encode(todos)
    case http.MethodPost:
        var t Todo
        if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        t.ID = len(todos) + 1
        todos = append(todos, t)
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(t)
    default:
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
    }
}

func handleTodoByID(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    idStr := r.URL.Path[len("/todos/"):]
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "invalid id", http.StatusBadRequest)
        return
    }

    for i, t := range todos {
        if t.ID == id {
            switch r.Method {
            case http.MethodGet:
                json.NewEncoder(w).Encode(t)
            case http.MethodDelete:
                todos = append(todos[:i], todos[i+1:]...)
                w.WriteHeader(http.StatusNoContent)
            default:
                http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
            }
            return
        }
    }

    http.Error(w, "not found", http.StatusNotFound)
}

This is one of the best examples if you want to understand the bare minimum: how to register handlers, read the path, decode JSON, and write responses. It’s not pretty, but it’s honest.


Using a router: examples include chi and gorilla-style APIs

Once you’ve built a couple of examples of REST API with Go: practical examples using net/http, you’ll probably want nicer routing. In 2024, the chi router is a popular choice for lightweight, idiomatic APIs.

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

type User struct {
    ID    int    `json:"id"`
    Email string `json:"email"`
}

var users = []User{{ID: 1, Email: "alice@example.com"}}

func main() {
    r := chi.NewRouter()

    // built-in middleware: logging, recovery, request IDs
    r.Use(middleware.RequestID)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)

    r.Route("/api", func(api chi.Router) {
        api.Get("/users", listUsers)
        api.Post("/users", createUser)
        api.Get("/users/{id}", getUser)
    })

    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    var u User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    u.ID = len(users) + 1
    users = append(users, u)

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(u)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    idStr := chi.URLParam(r, "id")
    // parse id, find user, return JSON (similar to todo example)
}

This example of a REST API with Go highlights:

  • Cleaner route definitions with path parameters.
  • Middleware for logging and panic recovery.
  • A structure that scales as you add more endpoints.

For real-world security considerations around user data, you can cross-check patterns with privacy guidance from organizations like the U.S. Federal Trade Commission.


JSON validation and error handling: real examples that won’t embarrass you in prod

Many tutorials skip validation. That’s how you end up with weird bugs and security issues. In this section, examples of REST API with Go focus on validating requests and returning consistent errors.

type CreateUserRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}

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

func writeError(w http.ResponseWriter, status int, code, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIError{Code: code, Message: msg})
}

func createUserValidated(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", "Invalid JSON body")
        return
    }

    if req.Email == "" || req.Password == "" {
        writeError(w, http.StatusBadRequest, "missing_fields", "Email and password are required")
        return
    }

    if len(req.Password) < 12 {
        writeError(w, http.StatusBadRequest, "weak_password", "Password must be at least 12 characters")
        return
    }

    // create user in DB here...

    w.WriteHeader(http.StatusCreated)
}

This is one of the best examples if you care about:

  • Clear separation between transport errors and business logic.
  • Consistent error shapes for frontend teams.
  • Simple but realistic password rules (inspired by security guidance from sources like NIST).

Database-backed examples of REST API with Go: practical examples with PostgreSQL

In reality, your API won’t stay in memory. Let’s wire up PostgreSQL using database/sql and pgx as the driver. This example of a REST API with Go shows a small “articles” service.

import (
    "context"
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "time"

    _ "github.com/jackc/pgx/v5/stdlib"
)

type Article struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Body      string    `json:"body"`
    CreatedAt time.Time `json:"created_at"`
}

type App struct {
    DB *sql.DB
}

func main() {
    db, err := sql.Open("pgx", "postgres://user:pass@localhost:5432/blog?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(5)

    app := &App{DB: db}

    http.HandleFunc("/articles", app.handleArticles)

    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func (a *App) handleArticles(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {
    case http.MethodGet:
        ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
        defer cancel()

        rows, err := a.DB.QueryContext(ctx,
            `SELECT id, title, body, created_at FROM articles ORDER BY created_at DESC LIMIT 50`)
        if err != nil {
            http.Error(w, "db error", http.StatusInternalServerError)
            return
        }
        defer rows.Close()

        var articles []Article
        for rows.Next() {
            var art Article
            if err := rows.Scan(&art.ID, &art.Title, &art.Body, &art.CreatedAt); err != nil {
                http.Error(w, "scan error", http.StatusInternalServerError)
                return
            }
            articles = append(articles, art)
        }

        json.NewEncoder(w).Encode(articles)

    case http.MethodPost:
        var req struct {
            Title string `json:"title"`
            Body  string `json:"body"`
        }
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
            http.Error(w, "invalid json", http.StatusBadRequest)
            return
        }

        ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
        defer cancel()

        var id int
        err := a.DB.QueryRowContext(ctx,
            `INSERT INTO articles (title, body) VALUES (\(1, \)2) RETURNING id`,
            req.Title, req.Body,
        ).Scan(&id)
        if err != nil {
            http.Error(w, "db error", http.StatusInternalServerError)
            return
        }

        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(map[string]int{"id": id})

    default:
        http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
    }
}

This is where examples of REST API with Go start to look like actual backend services: context timeouts, connection pools, and proper error handling.


Auth and JWT: examples include simple token-based APIs

Authentication is where many Go beginners get stuck. Let’s walk through a stripped-down but practical example of JWT auth middleware.

import (
    "context"
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt/v5"
)

type ctxKey string

const userIDKey ctxKey = "userID"

var jwtSecret = []byte("super-secret-key-change-me")

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
            http.Error(w, "missing token", http.StatusUnauthorized)
            return
        }

        tokenStr := strings.TrimPrefix(auth, "Bearer ")
        token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
            if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, jwt.ErrSignatureInvalid
            }
            return jwtSecret, nil
        })
        if err != nil || !token.Valid {
            http.Error(w, "invalid token", http.StatusUnauthorized)
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        if !ok {
            http.Error(w, "invalid claims", http.StatusUnauthorized)
            return
        }

        userID, _ := claims["sub"].(string)
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func CurrentUserHandler(w http.ResponseWriter, r *http.Request) {
    id, _ := r.Context().Value(userIDKey).(string)
    if id == "" {
        http.Error(w, "no user", http.StatusUnauthorized)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"user_id": id})
}

This example of REST API with Go shows:

  • How to plug authentication into a router as middleware.
  • How to pass user identity through context.Context.
  • How to build endpoints that rely on the authenticated user.

For security best practices around tokens and sessions, it’s worth reading guidance from organizations like OWASP.


Versioning and structuring: examples of REST API with Go for long-lived services

Once your API survives a few production releases, you’ll need versioning. Here’s a small but realistic layout using chi that shows how examples of REST API with Go can be organized for the long haul.

func Routes() http.Handler {
    r := chi.NewRouter()

    r.Use(middleware.RequestID)
    r.Use(middleware.Logger)

    r.Route("/api", func(api chi.Router) {
        api.Route("/v1", func(v1 chi.Router) {
            v1.Mount("/users", usersV1Routes())
            v1.Mount("/articles", articlesV1Routes())
        })

        api.Route("/v2", func(v2 chi.Router) {
            v2.Mount("/users", usersV2Routes()) // maybe different response shapes
        })
    })

    return r
}

func usersV1Routes() chi.Router {
    r := chi.NewRouter()
    r.Get("/", listUsersV1)
    r.Post("/", createUserV1)
    return r
}

func usersV2Routes() chi.Router {
    r := chi.NewRouter()
    r.Get("/", listUsersV2) // e.g., includes pagination metadata
    return r
}

This style gives you:

  • Clean separation between versions.
  • The ability to ship new behavior without breaking old clients.
  • A clear mental model when you add more examples of REST API with Go: practical examples over time.

Observability: real examples with logging, metrics, and health checks

Modern APIs live in noisy environments: Kubernetes, serverless, or managed platforms. You need observability baked in. Here are small, focused examples of REST API with Go that add health checks and metrics.

Health check handler

func healthHandler(w http.ResponseWriter, r *http.Request) {
    // In a real service, you might check DB, cache, external APIs, etc.
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "status": "ok",
    })
}

You can wire this to /healthz and let Kubernetes or your load balancer use it for readiness checks.

Prometheus metrics example

import (
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    r := chi.NewRouter()

    r.Handle("/metrics", promhttp.Handler())
    r.Get("/healthz", healthHandler)

    // other routes...

    log.Fatal(http.ListenAndServe(":8080", r))
}

These real examples show how a Go REST API can expose metrics that monitoring systems scrape. For background on why observability matters in production systems, the U.S. National Institute of Standards and Technology (NIST) publishes guidance on system reliability and monitoring that’s worth a look.


In 2024–2025, Go REST APIs are showing up in a few recurring patterns:

  • As lightweight microservices behind API gateways.
  • As backends for React/Next.js frontends.
  • As internal APIs orchestrating data pipelines and ML services.

The examples of REST API with Go: practical examples you’ve seen here map directly to those patterns:

  • The in-memory and router-based examples are perfect for small internal tools.
  • The PostgreSQL-backed example is basically a starter kit for a production CRUD service.
  • The JWT and versioning examples include the patterns you’ll see in real SaaS backends.
  • The observability examples plug into Prometheus, Grafana, and cloud monitoring.

If you’re building APIs that touch regulated data (for example, health data in the U.S.), it’s worth understanding standards like HIPAA and reading from health-focused sources such as the National Institutes of Health or Mayo Clinic to design endpoints and access controls responsibly.


FAQ: short, honest answers about Go REST APIs

What are some real examples of REST API with Go used in production?

Real-world examples include internal microservices at cloud providers, payment processing backends, analytics ingestion services, and high-traffic APIs in companies that value performance and simplicity. Many engineering blogs (for example, from large tech companies) describe migrating parts of their stack to Go for HTTP services.

Can you show an example of testing a REST API handler in Go?

Here’s a minimal test using net/http/httptest:

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestListUsers(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/users", nil)
    rr := httptest.NewRecorder()

    handler := http.HandlerFunc(listUsers)
    handler.ServeHTTP(rr, req)

    if rr.Code != http.StatusOK {
        t.Fatalf("expected 200, got %d", rr.Code)
    }

    if ct := rr.Header().Get("Content-Type"); ct != "application/json" {
        t.Fatalf("expected application/json, got %s", ct)
    }
}

This example of a test keeps everything in-memory and fast, which is exactly what you want for unit tests.

Are frameworks necessary, or can I stick to the standard library?

You can absolutely build production APIs using only net/http. Many of the best examples of REST API with Go in open source projects rely mostly on the standard library plus a router like chi. Frameworks can help, but they’re optional, not mandatory.

How do I document a Go REST API?

Popular options include OpenAPI/Swagger with generators that scan your handlers and comments. You can also maintain markdown docs by hand if your surface area is small. The key is to keep examples, request/response shapes, and error codes aligned with the actual handlers so that your examples of REST API with Go: practical examples remain trustworthy.

How do I handle pagination in Go REST APIs?

A common pattern is to accept page and page_size query parameters, apply LIMIT and OFFSET in SQL, and return metadata fields like total and next_page. This plays nicely with frontend frameworks and is easy to express in Go structs.


The bottom line: if you study and adapt these examples of REST API with Go: practical examples, you’ll have a solid starter kit for everything from hobby projects to serious production services.

Explore More Go Code Snippets

Discover more examples and insights in this category.

View All Go Code Snippets