API Design and Best Practices
Introduction
Good API design is crucial for building scalable, maintainable services. This guide covers versioning, documentation, rate limiting, and best practices for professional API development.
Core Concepts
API Design Principles
- Consistency: Uniform patterns across endpoints
- Clarity: Clear, intuitive URLs and responses
- Stability: Backward compatibility
- Security: Proper authentication and validation
- Performance: Efficient responses
Good: API Versioning
Versioning Strategies
package main
import (
"github.com/gin-gonic/gin"
)
// โ
GOOD: URL-based versioning
func setupVersionedAPI(router *gin.Engine) {
v1 := router.Group("/api/v1")
{
v1.GET("/users", listUsersV1)
v1.GET("/users/:id", getUserV1)
}
v2 := router.Group("/api/v2")
{
v2.GET("/users", listUsersV2)
v2.GET("/users/:id", getUserV2)
}
}
// โ
GOOD: Header-based versioning
func setupHeaderVersioning(router *gin.Engine) {
router.GET("/users", func(c *gin.Context) {
version := c.GetHeader("API-Version")
if version == "2" {
listUsersV2(c)
} else {
listUsersV1(c)
}
})
}
func listUsersV1(c *gin.Context) {
// V1 implementation
}
func listUsersV2(c *gin.Context) {
// V2 implementation with new fields
}
func getUserV1(c *gin.Context) {
// V1 implementation
}
func getUserV2(c *gin.Context) {
// V2 implementation
}
Good: Documentation
API Documentation
package main
import (
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// โ
GOOD: Swagger documentation
// @title User API
// @version 1.0
// @description This is a user management API
// @host localhost:8080
// @basePath /api/v1
// @Summary Get all users
// @Description Get a list of all users
// @Tags users
// @Accept json
// @Produce json
// @Success 200 {array} User
// @Router /users [get]
func listUsers(c *gin.Context) {
// Implementation
}
// @Summary Get user by ID
// @Description Get a specific user by ID
// @Tags users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} User
// @Failure 404 {object} ErrorResponse
// @Router /users/{id} [get]
func getUser(c *gin.Context) {
// Implementation
}
// โ
GOOD: Setup Swagger UI
func setupSwagger(router *gin.Engine) {
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
Good: Rate Limiting
Implementing Rate Limiting
package main
import (
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
"net/http"
)
// โ
GOOD: Rate limiting middleware
func rateLimitMiddleware(limiter *rate.Limiter) gin.HandlerFunc {
return func(c *gin.Context) {
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
})
c.Abort()
return
}
c.Next()
}
}
// โ
GOOD: Per-user rate limiting
func perUserRateLimitMiddleware() gin.HandlerFunc {
limiters := make(map[string]*rate.Limiter)
return func(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
userID = c.ClientIP()
}
limiter, exists := limiters[userID]
if !exists {
limiter = rate.NewLimiter(rate.Limit(10), 1)
limiters[userID] = limiter
}
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
})
c.Abort()
return
}
c.Next()
}
}
// โ
GOOD: Setup rate limiting
func setupRateLimiting(router *gin.Engine) {
limiter := rate.NewLimiter(rate.Limit(100), 1)
router.Use(rateLimitMiddleware(limiter))
}
Good: Error Handling
Consistent Error Responses
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// โ
GOOD: Structured error response
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
// โ
GOOD: Error codes
const (
ErrInvalidInput = "INVALID_INPUT"
ErrNotFound = "NOT_FOUND"
ErrUnauthorized = "UNAUTHORIZED"
ErrForbidden = "FORBIDDEN"
ErrConflict = "CONFLICT"
ErrInternalError = "INTERNAL_ERROR"
)
// โ
GOOD: Error handler
func handleError(c *gin.Context, statusCode int, code string, message string) {
c.JSON(statusCode, ErrorResponse{
Code: code,
Message: message,
})
}
// โ
GOOD: Validation error handler
func handleValidationError(c *gin.Context, errors map[string]string) {
c.JSON(http.StatusBadRequest, ErrorResponse{
Code: ErrInvalidInput,
Message: "Validation failed",
Details: errors,
})
}
Advanced Patterns
HATEOAS (Hypermedia)
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// โ
GOOD: HATEOAS response
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Links []Link `json:"_links"`
}
type Link struct {
Rel string `json:"rel"`
Href string `json:"href"`
}
func getUserWithLinks(c *gin.Context) {
id := c.Param("id")
user := UserResponse{
ID: id,
Name: "John Doe",
Email: "[email protected]",
Links: []Link{
{Rel: "self", Href: "/api/v1/users/" + id},
{Rel: "all", Href: "/api/v1/users"},
{Rel: "update", Href: "/api/v1/users/" + id},
{Rel: "delete", Href: "/api/v1/users/" + id},
},
}
c.JSON(http.StatusOK, user)
}
Content Negotiation
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// โ
GOOD: Content negotiation
func handleContentNegotiation(c *gin.Context) {
accept := c.GetHeader("Accept")
data := map[string]interface{}{
"id": 1,
"name": "John",
}
switch accept {
case "application/xml":
c.XML(http.StatusOK, data)
case "application/json":
fallthrough
default:
c.JSON(http.StatusOK, data)
}
}
Best Practices
1. Use Consistent Naming
// โ
GOOD: Consistent naming
// GET /api/v1/users
// POST /api/v1/users
// GET /api/v1/users/:id
// PUT /api/v1/users/:id
// DELETE /api/v1/users/:id
// โ BAD: Inconsistent naming
// GET /api/getUsers
// POST /api/createUser
// GET /api/user/:id
// PUT /api/updateUser/:id
// DELETE /api/removeUser/:id
2. Use Appropriate Status Codes
// โ
GOOD: Correct status codes
http.StatusOK // 200 - Success
http.StatusCreated // 201 - Created
http.StatusBadRequest // 400 - Invalid input
http.StatusUnauthorized // 401 - Auth required
http.StatusForbidden // 403 - Access denied
http.StatusNotFound // 404 - Not found
http.StatusConflict // 409 - Conflict
http.StatusInternalServerError // 500 - Server error
3. Document Your API
// โ
GOOD: Use Swagger/OpenAPI
// Document all endpoints
// Include request/response examples
// Specify error codes
// โ BAD: No documentation
// Users have to guess how to use the API
Resources
- REST API Best Practices: https://restfulapi.net/
- OpenAPI Specification: https://swagger.io/specification/
- API Design Guide: https://cloud.google.com/apis/design
Summary
Good API design requires consistency, clarity, and proper documentation. Use versioning for backward compatibility, implement rate limiting for protection, and provide comprehensive documentation. Following these practices ensures your APIs are professional, maintainable, and user-friendly.
Comments