← Home
gobackendapi

Building REST APIs in Go


Project Setup

Start by initializing a Go module and setting up the basic project structure.

Directory Structure

A clean project layout helps maintain code as it grows:

myapi/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── handler/
│   ├── middleware/
│   └── model/
├── go.mod
└── go.sum

Dependencies

Keep dependencies minimal. The standard library provides most of what you need:

go mod init github.com/example/myapi

Routing

Go 1.22+ includes an improved http.ServeMux with method and path parameter support.

Basic Routes

mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)

Path Parameters

Extract path parameters using r.PathValue():

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // fetch user by id...
}

Middleware

Middleware wraps handlers to add cross-cutting concerns like logging, auth, and CORS.

Logging Middleware

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("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

Authentication Middleware

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if !isValidToken(token) {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

CORS Middleware

Handle Cross-Origin Resource Sharing for frontend clients:

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Error Handling

Consistent error responses make your API easier to consume.

Error Response Format

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

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

Validation Errors

Return structured validation errors for bad requests:

func validateUser(u User) []string {
    var errs []string
    if u.Name == "" {
        errs = append(errs, "name is required")
    }
    if u.Email == "" {
        errs = append(errs, "email is required")
    }
    return errs
}

Testing

Write tests for your handlers using httptest.

Handler Tests

func TestGetUser(t *testing.T) {
    req := httptest.NewRequest("GET", "/api/users/1", nil)
    w := httptest.NewRecorder()
    getUser(w, req)

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

Integration Tests

Test the full request lifecycle:

func TestCreateUser(t *testing.T) {
    body := `{"name": "Alice", "email": "alice@example.com"}`
    req := httptest.NewRequest("POST", "/api/users", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()
    createUser(w, req)

    if w.Code != http.StatusCreated {
        t.Errorf("expected 201, got %d", w.Code)
    }
}

Deployment

Docker

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN go build -o server ./cmd/server

FROM alpine:3.19
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

Configuration

Use environment variables for production config:

port := os.Getenv("PORT")
if port == "" {
    port = "8080"
}
log.Printf("Starting server on :%s", port)
http.ListenAndServe(":"+port, mux)

Summary

Building REST APIs in Go is straightforward with the standard library. Focus on clean project structure, proper middleware chains, consistent error handling, and thorough testing.