Skip to main content
โšก Calmops

Building REST APIs with Go

Building REST APIs with Go

Introduction

REST APIs are the foundation of modern web services. This guide covers designing, building, and deploying REST APIs in Go using best practices and proven patterns.

Core Concepts

REST Principles

  • Resources: Nouns (users, posts, comments)
  • Methods: Verbs (GET, POST, PUT, DELETE)
  • Status Codes: Indicate operation result
  • Representations: JSON, XML, etc.

API Design

  • Use meaningful URLs
  • Use appropriate HTTP methods
  • Return proper status codes
  • Version your API
  • Document endpoints

Good: API Design

RESTful Endpoints

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// โœ… GOOD: RESTful endpoint design
func setupAPI(router *gin.Engine) {
	api := router.Group("/api/v1")

	// Users endpoints
	api.GET("/users", listUsers)           // Get all users
	api.POST("/users", createUser)         // Create user
	api.GET("/users/:id", getUser)         // Get specific user
	api.PUT("/users/:id", updateUser)      // Update user
	api.DELETE("/users/:id", deleteUser)   // Delete user

	// Posts endpoints
	api.GET("/users/:id/posts", getUserPosts)     // Get user's posts
	api.POST("/users/:id/posts", createUserPost)  // Create post for user
	api.GET("/posts/:id", getPost)                // Get specific post
	api.PUT("/posts/:id", updatePost)             // Update post
	api.DELETE("/posts/:id", deletePost)          // Delete post
}

// โœ… GOOD: Consistent response format
type APIResponse struct {
	Success bool        `json:"success"`
	Data    interface{} `json:"data,omitempty"`
	Error   string      `json:"error,omitempty"`
	Code    int         `json:"code"`
}

func respondSuccess(c *gin.Context, statusCode int, data interface{}) {
	c.JSON(statusCode, APIResponse{
		Success: true,
		Data:    data,
		Code:    statusCode,
	})
}

func respondError(c *gin.Context, statusCode int, message string) {
	c.JSON(statusCode, APIResponse{
		Success: false,
		Error:   message,
		Code:    statusCode,
	})
}

// โœ… GOOD: Implement endpoints
func listUsers(c *gin.Context) {
	users := []map[string]interface{}{
		{"id": 1, "name": "Alice"},
		{"id": 2, "name": "Bob"},
	}
	respondSuccess(c, http.StatusOK, users)
}

func createUser(c *gin.Context) {
	var user struct {
		Name  string `json:"name" binding:"required"`
		Email string `json:"email" binding:"required,email"`
	}

	if err := c.ShouldBindJSON(&user); err != nil {
		respondError(c, http.StatusBadRequest, err.Error())
		return
	}

	respondSuccess(c, http.StatusCreated, user)
}

func getUser(c *gin.Context) {
	id := c.Param("id")
	respondSuccess(c, http.StatusOK, map[string]string{"id": id})
}

func updateUser(c *gin.Context) {
	id := c.Param("id")
	respondSuccess(c, http.StatusOK, map[string]string{"id": id, "updated": "true"})
}

func deleteUser(c *gin.Context) {
	id := c.Param("id")
	respondSuccess(c, http.StatusOK, map[string]string{"id": id, "deleted": "true"})
}

func getUserPosts(c *gin.Context) {
	id := c.Param("id")
	respondSuccess(c, http.StatusOK, []string{})
}

func createUserPost(c *gin.Context) {
	respondSuccess(c, http.StatusCreated, map[string]string{"created": "true"})
}

func getPost(c *gin.Context) {
	id := c.Param("id")
	respondSuccess(c, http.StatusOK, map[string]string{"id": id})
}

func updatePost(c *gin.Context) {
	id := c.Param("id")
	respondSuccess(c, http.StatusOK, map[string]string{"id": id, "updated": "true"})
}

func deletePost(c *gin.Context) {
	id := c.Param("id")
	respondSuccess(c, http.StatusOK, map[string]string{"id": id, "deleted": "true"})
}

Good: Request Handling

Validation and Binding

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// โœ… GOOD: Request validation
type CreateUserRequest struct {
	Name  string `json:"name" binding:"required,min=3,max=50"`
	Email string `json:"email" binding:"required,email"`
	Age   int    `json:"age" binding:"min=0,max=150"`
}

func handleCreateUserWithValidation(c *gin.Context) {
	var req CreateUserRequest

	if err := c.ShouldBindJSON(&req); err != nil {
		respondError(c, http.StatusBadRequest, err.Error())
		return
	}

	// Process validated request
	respondSuccess(c, http.StatusCreated, req)
}

// โœ… GOOD: Query parameter validation
func handleSearch(c *gin.Context) {
	query := c.Query("q")
	if query == "" {
		respondError(c, http.StatusBadRequest, "q parameter required")
		return
	}

	limit := c.DefaultQuery("limit", "10")
	respondSuccess(c, http.StatusOK, map[string]string{
		"query": query,
		"limit": limit,
	})
}

// โœ… GOOD: Path parameter validation
func handleGetUserByID(c *gin.Context) {
	id := c.Param("id")
	if id == "" {
		respondError(c, http.StatusBadRequest, "id required")
		return
	}

	respondSuccess(c, http.StatusOK, map[string]string{"id": id})
}

Advanced Patterns

Pagination

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"strconv"
)

// โœ… GOOD: Pagination
type PaginationParams struct {
	Page     int `form:"page,default=1" binding:"min=1"`
	PageSize int `form:"page_size,default=10" binding:"min=1,max=100"`
}

func handlePaginatedList(c *gin.Context) {
	var params PaginationParams
	if err := c.ShouldBindQuery(&params); err != nil {
		respondError(c, http.StatusBadRequest, err.Error())
		return
	}

	// Calculate offset
	offset := (params.Page - 1) * params.PageSize

	// Fetch data
	data := map[string]interface{}{
		"page":       params.Page,
		"page_size":  params.PageSize,
		"offset":     offset,
		"items":      []string{},
		"total":      100,
	}

	respondSuccess(c, http.StatusOK, data)
}

Filtering and Sorting

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// โœ… GOOD: Filtering and sorting
type ListParams struct {
	Filter string `form:"filter"`
	Sort   string `form:"sort,default=created_at"`
	Order  string `form:"order,default=desc"`
}

func handleFilteredList(c *gin.Context) {
	var params ListParams
	c.ShouldBindQuery(&params)

	// Build query based on params
	data := map[string]interface{}{
		"filter": params.Filter,
		"sort":   params.Sort,
		"order":  params.Order,
		"items":  []string{},
	}

	respondSuccess(c, http.StatusOK, data)
}

Error Handling

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// โœ… GOOD: Structured error responses
type ErrorDetail struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

type ErrorResponse struct {
	Code    int            `json:"code"`
	Message string         `json:"message"`
	Details []ErrorDetail  `json:"details,omitempty"`
}

func respondValidationError(c *gin.Context, details []ErrorDetail) {
	c.JSON(http.StatusBadRequest, ErrorResponse{
		Code:    http.StatusBadRequest,
		Message: "Validation failed",
		Details: details,
	})
}

// โœ… GOOD: Consistent error handling
func handleWithErrorHandling(c *gin.Context) {
	// Simulate error
	respondValidationError(c, []ErrorDetail{
		{Field: "email", Message: "Invalid email format"},
		{Field: "age", Message: "Age must be between 0 and 150"},
	})
}

Best Practices

1. Use Appropriate Status Codes

// โœ… GOOD: Correct status codes
http.StatusOK              // 200 - Success
http.StatusCreated         // 201 - Resource created
http.StatusBadRequest      // 400 - Invalid input
http.StatusUnauthorized    // 401 - Auth required
http.StatusForbidden       // 403 - Access denied
http.StatusNotFound        // 404 - Resource not found
http.StatusConflict        // 409 - Conflict
http.StatusInternalServerError // 500 - Server error

2. Version Your API

// โœ… GOOD: API versioning
v1 := router.Group("/api/v1")
v2 := router.Group("/api/v2")

// โŒ BAD: No versioning
router.GET("/users", listUsers)

3. Use Consistent Response Format

// โœ… GOOD: Consistent format
{
  "success": true,
  "data": {...},
  "code": 200
}

// โŒ BAD: Inconsistent format
{
  "users": [...],
  "status": "ok"
}

Resources

Summary

Building REST APIs in Go requires careful attention to design, validation, and error handling. Use meaningful URLs, appropriate HTTP methods, and consistent response formats. Proper validation, pagination, and error handling make APIs robust and user-friendly.

Comments