Building REST APIs in Go
REST (Representational State Transfer) APIs are the backbone of modern web services. Go makes it easy to build fast, scalable REST APIs using the standard library or frameworks like Gin.
Basic REST API Structure
A typical REST API follows these conventions:
- Uses HTTP methods: GET, POST, PUT, DELETE
- Returns JSON responses
- Uses meaningful URLs
- Returns appropriate HTTP status codes
Simple REST API with net/http
Let’s build a basic API for managing users:
package main
import (
"encoding/json"
"net/http"
"strconv"
"sync"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
var (
users = make(map[int]User)
nextID = 1
mu sync.RWMutex
)
func getUsers(w http.ResponseWriter, r *http.Request) {
mu.RLock()
defer mu.RUnlock()
var userList []User
for _, user := range users {
userList = append(userList, user)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(userList)
}
func getUser(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/users/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
mu.RLock()
user, exists := users[id]
mu.RUnlock()
if !exists {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
mu.Lock()
user.ID = nextID
users[nextID] = user
nextID++
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/users/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
var updatedUser User
if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
mu.Lock()
if _, exists := users[id]; !exists {
mu.Unlock()
http.Error(w, "User not found", http.StatusNotFound)
return
}
updatedUser.ID = id
users[id] = updatedUser
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedUser)
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/users/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
mu.Lock()
if _, exists := users[id]; !exists {
mu.Unlock()
http.Error(w, "User not found", http.StatusNotFound)
return
}
delete(users, id)
mu.Unlock()
w.WriteHeader(http.StatusNoContent)
}
func main() {
// Routes
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
getUsers(w, r)
case "POST":
createUser(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
getUser(w, r)
case "PUT":
updateUser(w, r)
case "DELETE":
deleteUser(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
http.ListenAndServe(":8080", nil)
}Using a Router
For better organization, use a router like gorilla/mux:
package main
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
// Routes
r.HandleFunc("/users", getUsers).Methods("GET")
r.HandleFunc("/users", createUser).Methods("POST")
r.HandleFunc("/users/{id}", getUser).Methods("GET")
r.HandleFunc("/users/{id}", updateUser).Methods("PUT")
r.HandleFunc("/users/{id}", deleteUser).Methods("DELETE")
http.ListenAndServe(":8080", r)
}
// Handler functions (getUsers, createUser, etc.) remain the same
// but use mux.Vars(r)["id"] to get path parameters
Middleware
Add middleware for logging, CORS, authentication:
package main
import (
"log"
"net/http"
"time"
)
// Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Create a response writer wrapper to capture status code
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, rw.statusCode, time.Since(start))
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// CORS middleware
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, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
r := mux.NewRouter()
// API routes
api := r.PathPrefix("/api").Subrouter()
api.Use(loggingMiddleware)
api.Use(corsMiddleware)
api.HandleFunc("/users", getUsers).Methods("GET")
api.HandleFunc("/users", createUser).Methods("POST")
http.ListenAndServe(":8080", r)
}Input Validation
Validate incoming data:
package main
import (
"errors"
"strings"
)
type User struct {
ID int `json:"id"`
Name string `json:"name" validate:"required,min=2,max=50"`
Age int `json:"age" validate:"required,min=0,max=150"`
}
func validateUser(user *User) error {
if strings.TrimSpace(user.Name) == "" {
return errors.New("name is required")
}
if len(user.Name) < 2 || len(user.Name) > 50 {
return errors.New("name must be between 2 and 50 characters")
}
if user.Age < 0 || user.Age > 150 {
return errors.New("age must be between 0 and 150")
}
return nil
}
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := validateUser(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Create user...
}Error Handling
Standardize error responses:
package main
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func sendError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
errorResp := ErrorResponse{
Error: message,
Code: statusCode,
}
json.NewEncoder(w).Encode(errorResp)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
sendError(w, "Invalid JSON format", http.StatusBadRequest)
return
}
if err := validateUser(&user); err != nil {
sendError(w, err.Error(), http.StatusBadRequest)
return
}
// Create user logic...
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}Pagination
Implement pagination for list endpoints:
package main
type PaginatedResponse struct {
Data []User `json:"data"`
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}
func getUsers(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
page := 1
perPage := 10
if p := r.URL.Query().Get("page"); p != "" {
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
page = parsed
}
}
if pp := r.URL.Query().Get("per_page"); pp != "" {
if parsed, err := strconv.Atoi(pp); err == nil && parsed > 0 && parsed <= 100 {
perPage = parsed
}
}
// Get users with pagination
mu.RLock()
allUsers := make([]User, 0, len(users))
for _, user := range users {
allUsers = append(allUsers, user)
}
mu.RUnlock()
total := len(allUsers)
totalPages := (total + perPage - 1) / perPage
start := (page - 1) * perPage
end := start + perPage
if end > total {
end = total
}
var pageUsers []User
if start < total {
pageUsers = allUsers[start:end]
}
response := PaginatedResponse{
Data: pageUsers,
Page: page,
PerPage: perPage,
Total: total,
TotalPages: totalPages,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}Testing REST APIs
Test your API endpoints:
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
)
func TestCreateUser(t *testing.T) {
// Reset global state for test
mu.Lock()
users = make(map[int]User)
nextID = 1
mu.Unlock()
// Create test data
userData := `{"name": "Test User", "age": 25}`
// Create request
req := httptest.NewRequest("POST", "/users", bytes.NewBufferString(userData))
req.Header.Set("Content-Type", "application/json")
// Create response recorder
w := httptest.NewRecorder()
// Call handler
createUser(w, req)
// Check response
if w.Code != http.StatusCreated {
t.Errorf("Expected status 201, got %d", w.Code)
}
// Check if user was created
mu.RLock()
if len(users) != 1 {
t.Errorf("Expected 1 user, got %d", len(users))
}
mu.RUnlock()
}API Documentation
Document your API using OpenAPI/Swagger or simple comments:
// GET /users
// Returns a list of all users
// Query parameters:
// - page: Page number (default: 1)
// - per_page: Items per page (default: 10, max: 100)
func getUsers(w http.ResponseWriter, r *http.Request) {
// Implementation...
}
// POST /users
// Creates a new user
// Request body: {"name": "string", "age": "number"}
// Returns: Created user with ID
func createUser(w http.ResponseWriter, r *http.Request) {
// Implementation...
}Best Practices
- Use consistent HTTP status codes: 200 for success, 201 for created, 400 for bad request, 404 for not found, 500 for server errors
- Validate input data: Always validate and sanitize user input
- Use JSON for data exchange: Standard format for APIs
- Implement proper error handling: Return meaningful error messages
- Add logging and monitoring: Track API usage and errors
- Version your API: Use URL versioning like
/api/v1/users - Use middleware: For cross-cutting concerns like authentication, logging
- Document your API: Make it easy for others to use
- Test thoroughly: Unit tests, integration tests, and end-to-end tests
- Consider rate limiting: Protect against abuse
Building REST APIs in Go is straightforward with the standard library, and you can enhance functionality with routers and middleware. For more complex applications, consider using frameworks like Gin or Echo.
For more on web servers, see our building web servers tutorial. If you’re building APIs with Gin, check out the Gin framework tutorial.