Skip to main content
โšก Calmops

API Design and Best Practices

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

  1. Consistency: Uniform patterns across endpoints
  2. Clarity: Clear, intuitive URLs and responses
  3. Stability: Backward compatibility
  4. Security: Proper authentication and validation
  5. 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

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