Designing Your REST API with Go: A Complete Guide

If you’ve been exploring client-side Go development, it’s time to dive into the server side. Designing a REST API is one of the core skills any Go developer should master. In this guide, we’ll cover everything from HTTP methods and routing to middleware, versioning, and building a full CRUD API. By the end, you’ll be ready to implement a robust REST API in Go.

We’ll cover:

  • HTTP methods and status codes
  • Routing and URL design
  • Middleware for logging and authentication
  • API versioning strategies
  • Building a CRUD API with Go

Requirements: Go version 1.22+ and cURL installed.


Designing Your REST API with Go

Understanding HTTP Methods and Status Codes

REST APIs revolve around HTTP methods, also known as verbs. They define what action the client wants to perform on a resource. The most common methods in REST APIs are:

  • GET – Retrieve data
  • POST – Create a new resource
  • PUT – Update an existing resource fully
  • PATCH – Partially update an existing resource
  • DELETE – Remove a resource

GET – Retrieving Resources

The GET method is used to fetch data from the server. It’s idempotent, meaning repeated requests should return the same result.

Example – Get a single user:

$ curl https://api.example.com/users/1
{
  "id": 1,
  "username": "john.doe",
  "name": "John",
  "surname": "Doe"
}

Example – Get a list of users:

$ curl https://api.example.com/users
[
  {"id": 1, "username": "john.doe", "name": "John", "surname": "Doe"},
  {"id": 2, "username": "jane.doe", "name": "Jane", "surname": "Doe"}
]

Query Parameters: You can filter, order, and limit resources:

$ curl https://api.example.com/users?filter=John&orderby=name&limit=10

POST – Creating Resources

The POST method creates new resources on the server.

$ curl -X POST https://api.example.com/users --data '{"username": "jesus.espino", "name": "Jesus", "surname": "Espino"}'

The server responds with the newly created resource including its ID.

Pro Tip: POST can also trigger actions, like sending notifications or login/logout actions.

PUT & PATCH – Updating Resources

  • PUT – Full replacement of a resource:
$ curl -X PUT https://api.example.com/users/3 --data '{"username": "jesus.espino", "name": "Jesús", "surname": "Espino García"}'
  • PATCH – Partial update (only modify certain fields):
$ curl -X PATCH https://api.example.com/users/3 -d '{"surname": "Other"}'

DELETE – Removing Resources

The DELETE method removes a resource:

$ curl -X DELETE https://api.example.com/users/3

It usually returns a 204 No Content status code.


HTTP Status Codes

Status codes tell the client the result of their request:

  • 2XX – Success
    • 200 OK, 201 Created, 204 No Content
  • 3XX – Redirection
    • 301 Moved Permanently, 302 Found, 304 Not Modified
  • 4XX – Client errors
    • 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
  • 5XX – Server errors
    • 500 Internal Server Error, 501 Not Implemented, 503 Service Unavailable

For a complete list: MDN HTTP Status Codes


Routing in Go

Routing defines how URLs map to functions. In Go, you can use the standard library or third-party packages like gorilla/mux or gin.

Example using Go 1.22 standard library:

http.HandleFunc("GET /api/v1/users", listUsers)
http.HandleFunc("POST /api/v1/users", createUsers)
http.HandleFunc("GET /api/v1/users/{id}", getUser)
http.HandleFunc("PUT /api/v1/users/{id}", updateUser)
http.HandleFunc("PATCH /api/v1/users/{id}", partialUpdateUser)
http.HandleFunc("DELETE /api/v1/users/{id}", deleteUser)

Example listUsers Handler

func listUsers(w http.ResponseWriter, r *http.Request) {
    users := []User{
        {ID: 1, Username: "john.doe", Name: "John", Surname: "Doe"},
        {ID: 2, Username: "jane.doe", Name: "Jane", Surname: "Doe"},
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(users)
}

Middleware

Middleware functions wrap handlers to add common functionality, like logging or authentication.

func logRequest(next http.HandlerFunc) http.HandlerFunc { 
  return func(w http.ResponseWriter, r *http.Request) {
    log.Printf("Before %s %s", r.Method, r.URL.Path)
    next(w, r)
    log.Printf("After %s %s", r.Method, r.URL.Path)
  }
}
http.HandleFunc("GET /api/v1/users", logRequest(listUsers))

You can chain multiple middlewares using the decorator pattern.


API Versioning

Always version your API. Even if you think you’re doing it right, breaking changes happen.

Example:

  • /api/v1/users – current version
  • /api/v2/users – new version with changes

Building a CRUD API in Go

Let’s create a shopping list API.

Resource Structure

type ShoppingList struct {
    ID int `json:"id"`
    Name string `json:"name"`
    Items []string `json:"items"`
}
var allData []ShoppingList

Endpoints

  1. Create a list – POST /v1/lists
  2. List all lists – GET /v1/lists
  3. Retrieve a single list – GET /v1/lists/{id}
  4. Update a list – PUT /v1/lists/{id}
  5. Partial update – PATCH /v1/lists/{id}
  6. Delete a list – DELETE /v1/lists/{id}
  7. Add item to list – POST /v1/lists/{id}/push

Each handler decodes JSON, updates allData, and returns the correct status code.


Testing with cURL

Create a list:

$ curl -X POST http://localhost:8888/v1/lists --data '{"id":1,"name":"my first shopping list","items":["eggs","milk"]}'

List all shopping lists:

$ curl http://localhost:8888/v1/lists

Update partially:

$ curl -X PATCH http://localhost:8888/v1/lists/1 --data '{"name":"updated shopping list"}'

Delete a list:

$ curl -X DELETE http://localhost:8888/v1/lists/1

Add an item to a list:

$ curl -X POST http://localhost:8888/v1/lists/1/push --data '{"item":"bread"}'

Summary

By now, you should understand:

  • HTTP methods and when to use them
  • Status codes for client-server communication
  • Routing and handler functions in Go
  • How to use middleware for cross-cutting concerns
  • API versioning best practices
  • Building a fully functional CRUD API

Best Practices for REST API Design in Go

Designing a working API is one thing—but building a robust, maintainable, and secure API is another. Here are some best practices to take your Go REST API to the next level.


1. Consistent Error Handling

Always respond with consistent error messages and appropriate status codes. This makes it easier for clients to understand what went wrong.

Example – error response format:

{
  "error": "User not found",
  "code": 404
}

Handler Example:

func respondWithError(w http.ResponseWriter, code int, message string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(map[string]string{"error": message})
}

Then in your handlers:

if listNotFound {
    respondWithError(w, http.StatusNotFound, "List not found")
}

2. Logging

Logging is crucial for debugging and monitoring your API.

  • Use structured logging (e.g., logrus or zap) instead of plain text logs.
  • Log request method, path, client IP, and any errors.
  • Avoid logging sensitive data like passwords or tokens.

Example middleware:

func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("Request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
        next(w, r)
    }
}

3. Secure Your API

Even simple APIs should follow basic security principles:

  • Authentication & Authorization: Use JWTs, OAuth2, or session-based auth.
  • Input validation: Always validate incoming JSON or query parameters.
  • Avoid exposing internal errors: Don’t leak stack traces to clients.
  • HTTPS only: Encrypt traffic to prevent data leaks.
  • Rate limiting: Protect against abuse with throttling.

4. Pagination, Filtering, and Sorting

When returning lists, avoid sending huge datasets. Use query parameters for:

  • Pagination: ?page=2&limit=20
  • Filtering: ?filter=name:John
  • Sorting: ?sort=name:asc

This makes your API scalable and friendly for front-end clients.


5. Use Context for Request Lifecycle

Go’s context.Context is vital for managing timeouts, cancellations, and deadlines.

func handleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    select {
    case <-time.After(2 * time.Second):
        fmt.Fprintln(w, "Processed")
    case <-ctx.Done():
        fmt.Fprintln(w, "Request cancelled")
    }
}

6. Version Your API Early

Even if your API is simple now, versioning protects against breaking changes. Always start with /v1/ in your URLs. If you need new functionality, create /v2/ instead of changing /v1/.


7. Testing Your API

Automated testing ensures your API behaves as expected.

  • Unit tests: Test handlers and business logic independently.
  • Integration tests: Test the full HTTP lifecycle.
  • Tools: httptest in Go, Postman, or curl scripts.

Example:

func TestListUsers(t *testing.T) {
    req, _ := http.NewRequest("GET", "/v1/users", nil)
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(listUsers)
    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("expected %v got %v", http.StatusOK, status)
    }
}

8. Use Middleware Wisely

Middleware isn’t just for logging:

  • Authentication – validate JWTs or sessions
  • Rate limiting – prevent abuse
  • CORS headers – allow front-end apps to access the API
  • Request/Response compression – reduce bandwidth

Chain multiple middlewares for clean, reusable code:

http.HandleFunc("/v1/users", loggingMiddleware(authMiddleware(listUsers)))

9. Documentation is Key

  • Use OpenAPI / Swagger for automated docs.
  • Keep examples up to date.
  • Include status codes, request/response formats, and error messages.

Good docs = happy developers = fewer support tickets.


10. Keep It Simple, Yet Flexible

  • Start small and refactor. Don’t over-engineer.
  • Avoid monolithic handlers; break logic into smaller functions.
  • Use interfaces to abstract storage (e.g., DB vs in-memory) for easier testing.

This extended section transforms your REST API guide from “basic tutorial” to a professional developer resource, giving readers the knowledge to build scalable, secure, and maintainable APIs in Go.

Hello! I'm a gaming enthusiast, a history buff, a cinema lover, connected to the news, and I enjoy exploring different lifestyles. I'm Yaman Şener/trioner.com, a web content creator who brings all these interests together to offer readers in-depth analyses, informative content, and inspiring perspectives. I'm here to accompany you through the vast spectrum of the digital world.

Leave a Reply

Your email address will not be published. Required fields are marked *