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(¶ms); 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(¶ms)
// 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
- REST API Best Practices: https://restfulapi.net/
- HTTP Status Codes: https://httpwg.org/specs/rfc7231.html#status.codes
- API Design Guide: https://swagger.io/resources/articles/best-practices-in-api-design/
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