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)
}
2. Filtering and Search
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
- Learn about GraphQL
- Explore WebSockets
- Study Authentication