system design
System Design Fundamentals: API Design Patterns
#System Design#API Design#REST#GraphQL#gRPC
Comprehensive guide to API design - REST, GraphQL, gRPC, WebSocket, and best practices for building scalable, maintainable APIs.
System Design Fundamentals: API Design Patterns
APIs (Application Programming Interfaces) are the contracts between services in distributed systems. Well-designed APIs are crucial for system scalability, maintainability, and developer experience.
API Design Paradigms
graph TD
A[API Design Patterns] --> B[REST]
A --> C[GraphQL]
A --> D[gRPC]
A --> E[WebSocket]
B --> B1[Resource-Based]
B --> B2[HTTP Methods]
B --> B3[Stateless]
C --> C1[Query Language]
C --> C2[Single Endpoint]
C --> C3[Client-Driven]
D --> D1[Protocol Buffers]
D --> D2[HTTP/2]
D --> D3[Streaming]
E --> E1[Real-Time]
E --> E2[Bidirectional]
E --> E3[Persistent Connection]
style B fill:#e8f5e8
style C fill:#e1f5fe
style D fill:#fff3e0
style E fill:#f3e5f5
RESTful API Implementation
// Complete REST API Implementation
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
// Domain Models
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
}
// Repository Pattern
type UserRepository struct {
users map[string]*User
mutex sync.RWMutex
}
func NewUserRepository() *UserRepository {
return &UserRepository{
users: make(map[string]*User),
}
}
func (r *UserRepository) Create(user *User) error {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, exists := r.users[user.ID]; exists {
return fmt.Errorf("user already exists")
}
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
r.users[user.ID] = user
return nil
}
func (r *UserRepository) GetByID(id string) (*User, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
user, exists := r.users[id]
if !exists {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func (r *UserRepository) GetAll(limit, offset int) ([]*User, int) {
r.mutex.RLock()
defer r.mutex.RUnlock()
users := make([]*User, 0, len(r.users))
for _, user := range r.users {
users = append(users, user)
}
total := len(users)
// Apply pagination
start := offset
end := offset + limit
if start > total {
return []*User{}, total
}
if end > total {
end = total
}
return users[start:end], total
}
func (r *UserRepository) Update(id string, updates map[string]interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()
user, exists := r.users[id]
if !exists {
return fmt.Errorf("user not found")
}
// Apply updates
if username, ok := updates["username"].(string); ok {
user.Username = username
}
if email, ok := updates["email"].(string); ok {
user.Email = email
}
user.UpdatedAt = time.Now()
return nil
}
func (r *UserRepository) Delete(id string) error {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, exists := r.users[id]; !exists {
return fmt.Errorf("user not found")
}
delete(r.users, id)
return nil
}
// API Response Types
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error *APIError `json:"error,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type Meta struct {
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
}
// REST API Handler
type RESTAPIHandler struct {
userRepo *UserRepository
}
func NewRESTAPIHandler(userRepo *UserRepository) *RESTAPIHandler {
return &RESTAPIHandler{
userRepo: userRepo,
}
}
func (h *RESTAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
// Route handling
path := strings.TrimPrefix(r.URL.Path, "/api/v1")
switch {
case strings.HasPrefix(path, "/users"):
h.handleUsers(w, r, path)
case strings.HasPrefix(path, "/health"):
h.handleHealth(w, r)
default:
h.sendError(w, http.StatusNotFound, "NOT_FOUND", "Endpoint not found")
}
}
func (h *RESTAPIHandler) handleUsers(w http.ResponseWriter, r *http.Request, path string) {
// Extract user ID if present
pathParts := strings.Split(strings.TrimPrefix(path, "/users"), "/")
var userID string
if len(pathParts) > 1 && pathParts[1] != "" {
userID = pathParts[1]
}
switch r.Method {
case "GET":
if userID != "" {
h.getUser(w, r, userID)
} else {
h.listUsers(w, r)
}
case "POST":
h.createUser(w, r)
case "PUT", "PATCH":
if userID != "" {
h.updateUser(w, r, userID)
} else {
h.sendError(w, http.StatusBadRequest, "BAD_REQUEST", "User ID required")
}
case "DELETE":
if userID != "" {
h.deleteUser(w, r, userID)
} else {
h.sendError(w, http.StatusBadRequest, "BAD_REQUEST", "User ID required")
}
default:
h.sendError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
}
}
func (h *RESTAPIHandler) getUser(w http.ResponseWriter, r *http.Request, id string) {
user, err := h.userRepo.GetByID(id)
if err != nil {
h.sendError(w, http.StatusNotFound, "USER_NOT_FOUND", err.Error())
return
}
h.sendSuccess(w, http.StatusOK, user, nil)
}
func (h *RESTAPIHandler) listUsers(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
query := r.URL.Query()
page := 1
perPage := 10
if p := query.Get("page"); p != "" {
fmt.Sscanf(p, "%d", &page)
}
if pp := query.Get("per_page"); pp != "" {
fmt.Sscanf(pp, "%d", &perPage)
}
offset := (page - 1) * perPage
users, total := h.userRepo.GetAll(perPage, offset)
totalPages := (total + perPage - 1) / perPage
meta := &Meta{
Total: total,
Page: page,
PerPage: perPage,
TotalPages: totalPages,
}
h.sendSuccess(w, http.StatusOK, users, meta)
}
func (h *RESTAPIHandler) createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
h.sendError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid request body")
return
}
// Generate ID
user.ID = fmt.Sprintf("user-%d", time.Now().UnixNano())
if err := h.userRepo.Create(&user); err != nil {
h.sendError(w, http.StatusConflict, "USER_EXISTS", err.Error())
return
}
h.sendSuccess(w, http.StatusCreated, user, nil)
}
func (h *RESTAPIHandler) updateUser(w http.ResponseWriter, r *http.Request, id string) {
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
h.sendError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid request body")
return
}
if err := h.userRepo.Update(id, updates); err != nil {
h.sendError(w, http.StatusNotFound, "USER_NOT_FOUND", err.Error())
return
}
user, _ := h.userRepo.GetByID(id)
h.sendSuccess(w, http.StatusOK, user, nil)
}
func (h *RESTAPIHandler) deleteUser(w http.ResponseWriter, r *http.Request, id string) {
if err := h.userRepo.Delete(id); err != nil {
h.sendError(w, http.StatusNotFound, "USER_NOT_FOUND", err.Error())
return
}
h.sendSuccess(w, http.StatusNoContent, nil, nil)
}
func (h *RESTAPIHandler) handleHealth(w http.ResponseWriter, r *http.Request) {
health := map[string]interface{}{
"status": "healthy",
"timestamp": time.Now(),
"version": "1.0.0",
}
h.sendSuccess(w, http.StatusOK, health, nil)
}
func (h *RESTAPIHandler) sendSuccess(w http.ResponseWriter, status int, data interface{}, meta *Meta) {
response := APIResponse{
Success: true,
Data: data,
Meta: meta,
}
w.WriteHeader(status)
json.NewEncoder(w).Encode(response)
}
func (h *RESTAPIHandler) sendError(w http.ResponseWriter, status int, code, message string) {
response := APIResponse{
Success: false,
Error: &APIError{
Code: code,
Message: message,
},
}
w.WriteHeader(status)
json.NewEncoder(w).Encode(response)
}
GraphQL API Implementation
// GraphQL API Implementation
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
)
// GraphQL Schema Types
type GraphQLSchema struct {
userRepo *UserRepository
}
type GraphQLRequest struct {
Query string `json:"query"`
Variables map[string]interface{} `json:"variables"`
}
type GraphQLResponse struct {
Data interface{} `json:"data,omitempty"`
Errors []GraphQLError `json:"errors,omitempty"`
}
type GraphQLError struct {
Message string `json:"message"`
Path []string `json:"path,omitempty"`
}
func NewGraphQLSchema(userRepo *UserRepository) *GraphQLSchema {
return &GraphQLSchema{
userRepo: userRepo,
}
}
func (s *GraphQLSchema) Execute(ctx context.Context, query string, variables map[string]interface{}) *GraphQLResponse {
// Parse query (simplified parser)
query = strings.TrimSpace(query)
// Detect query type
if strings.Contains(query, "query") {
return s.executeQuery(ctx, query, variables)
} else if strings.Contains(query, "mutation") {
return s.executeMutation(ctx, query, variables)
}
return &GraphQLResponse{
Errors: []GraphQLError{{Message: "Invalid query"}},
}
}
func (s *GraphQLSchema) executeQuery(ctx context.Context, query string, variables map[string]interface{}) *GraphQLResponse {
// Simplified query execution
if strings.Contains(query, "user(id:") {
// Single user query
id := s.extractID(query)
user, err := s.userRepo.GetByID(id)
if err != nil {
return &GraphQLResponse{
Errors: []GraphQLError{{Message: err.Error()}},
}
}
return &GraphQLResponse{
Data: map[string]interface{}{"user": user},
}
} else if strings.Contains(query, "users") {
// List users query
limit := 10
offset := 0
if l, ok := variables["limit"].(float64); ok {
limit = int(l)
}
if o, ok := variables["offset"].(float64); ok {
offset = int(o)
}
users, total := s.userRepo.GetAll(limit, offset)
return &GraphQLResponse{
Data: map[string]interface{}{
"users": map[string]interface{}{
"nodes": users,
"total": total,
},
},
}
}
return &GraphQLResponse{
Errors: []GraphQLError{{Message: "Unknown query"}},
}
}
func (s *GraphQLSchema) executeMutation(ctx context.Context, query string, variables map[string]interface{}) *GraphQLResponse {
if strings.Contains(query, "createUser") {
// Create user mutation
username, _ := variables["username"].(string)
email, _ := variables["email"].(string)
user := &User{
ID: fmt.Sprintf("user-%d", time.Now().UnixNano()),
Username: username,
Email: email,
}
if err := s.userRepo.Create(user); err != nil {
return &GraphQLResponse{
Errors: []GraphQLError{{Message: err.Error()}},
}
}
return &GraphQLResponse{
Data: map[string]interface{}{"createUser": user},
}
} else if strings.Contains(query, "updateUser") {
// Update user mutation
id, _ := variables["id"].(string)
updates := make(map[string]interface{})
if username, ok := variables["username"].(string); ok {
updates["username"] = username
}
if email, ok := variables["email"].(string); ok {
updates["email"] = email
}
if err := s.userRepo.Update(id, updates); err != nil {
return &GraphQLResponse{
Errors: []GraphQLError{{Message: err.Error()}},
}
}
user, _ := s.userRepo.GetByID(id)
return &GraphQLResponse{
Data: map[string]interface{}{"updateUser": user},
}
}
return &GraphQLResponse{
Errors: []GraphQLError{{Message: "Unknown mutation"}},
}
}
func (s *GraphQLSchema) extractID(query string) string {
// Simplified ID extraction
start := strings.Index(query, "id:")
if start == -1 {
return ""
}
start += 3
end := strings.IndexAny(query[start:], " )")
if end == -1 {
return ""
}
id := strings.Trim(query[start:start+end], `"`)
return id
}
// GraphQL HTTP Handler
type GraphQLHandler struct {
schema *GraphQLSchema
}
func NewGraphQLHandler(schema *GraphQLSchema) *GraphQLHandler {
return &GraphQLHandler{
schema: schema,
}
}
func (h *GraphQLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req GraphQLRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response := GraphQLResponse{
Errors: []GraphQLError{{Message: "Invalid request body"}},
}
json.NewEncoder(w).Encode(response)
return
}
response := h.schema.Execute(r.Context(), req.Query, req.Variables)
json.NewEncoder(w).Encode(response)
}
gRPC API Implementation
// gRPC Service Implementation
package main
import (
"context"
"fmt"
"time"
)
// Protocol Buffer Message Definitions (simulated)
type CreateUserRequest struct {
Username string
Email string
}
type UpdateUserRequest struct {
Id string
Username string
Email string
}
type GetUserRequest struct {
Id string
}
type ListUsersRequest struct {
PageSize int32
PageToken string
}
type UserResponse struct {
Id string
Username string
Email string
CreatedAt int64
UpdatedAt int64
}
type ListUsersResponse struct {
Users []*UserResponse
NextPageToken string
TotalSize int32
}
type DeleteUserRequest struct {
Id string
}
type Empty struct{}
// gRPC Service Interface
type UserServiceServer interface {
CreateUser(context.Context, *CreateUserRequest) (*UserResponse, error)
GetUser(context.Context, *GetUserRequest) (*UserResponse, error)
UpdateUser(context.Context, *UpdateUserRequest) (*UserResponse, error)
DeleteUser(context.Context, *DeleteUserRequest) (*Empty, error)
ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error)
}
// gRPC Service Implementation
type UserServiceImpl struct {
userRepo *UserRepository
}
func NewUserServiceImpl(userRepo *UserRepository) *UserServiceImpl {
return &UserServiceImpl{
userRepo: userRepo,
}
}
func (s *UserServiceImpl) CreateUser(ctx context.Context, req *CreateUserRequest) (*UserResponse, error) {
user := &User{
ID: fmt.Sprintf("user-%d", time.Now().UnixNano()),
Username: req.Username,
Email: req.Email,
}
if err := s.userRepo.Create(user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return s.userToResponse(user), nil
}
func (s *UserServiceImpl) GetUser(ctx context.Context, req *GetUserRequest) (*UserResponse, error) {
user, err := s.userRepo.GetByID(req.Id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
return s.userToResponse(user), nil
}
func (s *UserServiceImpl) UpdateUser(ctx context.Context, req *UpdateUserRequest) (*UserResponse, error) {
updates := make(map[string]interface{})
if req.Username != "" {
updates["username"] = req.Username
}
if req.Email != "" {
updates["email"] = req.Email
}
if err := s.userRepo.Update(req.Id, updates); err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
user, _ := s.userRepo.GetByID(req.Id)
return s.userToResponse(user), nil
}
func (s *UserServiceImpl) DeleteUser(ctx context.Context, req *DeleteUserRequest) (*Empty, error) {
if err := s.userRepo.Delete(req.Id); err != nil {
return nil, fmt.Errorf("failed to delete user: %w", err)
}
return &Empty{}, nil
}
func (s *UserServiceImpl) ListUsers(ctx context.Context, req *ListUsersRequest) (*ListUsersResponse, error) {
pageSize := int(req.PageSize)
if pageSize == 0 {
pageSize = 10
}
offset := 0
// In real implementation, decode pageToken to get offset
users, total := s.userRepo.GetAll(pageSize, offset)
responses := make([]*UserResponse, len(users))
for i, user := range users {
responses[i] = s.userToResponse(user)
}
return &ListUsersResponse{
Users: responses,
NextPageToken: fmt.Sprintf("token-%d", offset+pageSize),
TotalSize: int32(total),
}, nil
}
func (s *UserServiceImpl) userToResponse(user *User) *UserResponse {
return &UserResponse{
Id: user.ID,
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt.Unix(),
UpdatedAt: user.UpdatedAt.Unix(),
}
}
// gRPC Streaming Implementation
type StreamingService struct {
userRepo *UserRepository
}
func NewStreamingService(userRepo *UserRepository) *StreamingService {
return &StreamingService{
userRepo: userRepo,
}
}
// Server Streaming: Watch user updates
func (s *StreamingService) WatchUsers(ctx context.Context, req *Empty, stream chan *UserResponse) error {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
// Get all users and stream them
users, _ := s.userRepo.GetAll(100, 0)
for _, user := range users {
select {
case stream <- (&UserServiceImpl{}).userToResponse(user):
case <-ctx.Done():
return ctx.Err()
}
}
}
}
}
// Bidirectional Streaming: Chat-like updates
type UserUpdate struct {
UserId string
Field string
Value string
}
func (s *StreamingService) StreamUserUpdates(ctx context.Context, updates chan *UserUpdate, responses chan *UserResponse) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case update := <-updates:
// Apply update
updateMap := map[string]interface{}{
update.Field: update.Value,
}
if err := s.userRepo.Update(update.UserId, updateMap); err != nil {
continue
}
// Send back updated user
user, err := s.userRepo.GetByID(update.UserId)
if err != nil {
continue
}
select {
case responses <- (&UserServiceImpl{}).userToResponse(user):
case <-ctx.Done():
return ctx.Err()
}
}
}
}
WebSocket API Implementation
// WebSocket Real-Time API
package main
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
// WebSocket Connection (simplified)
type WebSocketConnection struct {
ID string
UserID string
send chan []byte
hub *WebSocketHub
mutex sync.Mutex
}
type WebSocketHub struct {
connections map[string]*WebSocketConnection
broadcast chan *WebSocketMessage
register chan *WebSocketConnection
unregister chan *WebSocketConnection
mutex sync.RWMutex
}
type WebSocketMessage struct {
Type string `json:"type"`
Payload map[string]interface{} `json:"payload"`
Timestamp time.Time `json:"timestamp"`
}
func NewWebSocketHub() *WebSocketHub {
hub := &WebSocketHub{
connections: make(map[string]*WebSocketConnection),
broadcast: make(chan *WebSocketMessage, 256),
register: make(chan *WebSocketConnection),
unregister: make(chan *WebSocketConnection),
}
go hub.run()
return hub
}
func (h *WebSocketHub) run() {
for {
select {
case conn := <-h.register:
h.mutex.Lock()
h.connections[conn.ID] = conn
h.mutex.Unlock()
fmt.Printf("🔌 WebSocket connected: %s (total: %d)\n", conn.ID, len(h.connections))
// Send welcome message
welcome := &WebSocketMessage{
Type: "connected",
Payload: map[string]interface{}{
"connection_id": conn.ID,
"timestamp": time.Now(),
},
Timestamp: time.Now(),
}
conn.send <- h.messageToBytes(welcome)
case conn := <-h.unregister:
h.mutex.Lock()
if _, ok := h.connections[conn.ID]; ok {
delete(h.connections, conn.ID)
close(conn.send)
}
h.mutex.Unlock()
fmt.Printf("🔌 WebSocket disconnected: %s (total: %d)\n", conn.ID, len(h.connections))
case message := <-h.broadcast:
h.mutex.RLock()
connections := make([]*WebSocketConnection, 0, len(h.connections))
for _, conn := range h.connections {
connections = append(connections, conn)
}
h.mutex.RUnlock()
messageBytes := h.messageToBytes(message)
for _, conn := range connections {
select {
case conn.send <- messageBytes:
default:
// Connection buffer full, close it
h.unregister <- conn
}
}
}
}
}
func (h *WebSocketHub) messageToBytes(msg *WebSocketMessage) []byte {
bytes, _ := json.Marshal(msg)
return bytes
}
func (h *WebSocketHub) BroadcastUserUpdate(userID string, action string, data interface{}) {
message := &WebSocketMessage{
Type: "user_update",
Payload: map[string]interface{}{
"user_id": userID,
"action": action,
"data": data,
},
Timestamp: time.Now(),
}
h.broadcast <- message
}
func (h *WebSocketHub) SendToUser(userID string, messageType string, payload map[string]interface{}) {
h.mutex.RLock()
defer h.mutex.RUnlock()
message := &WebSocketMessage{
Type: messageType,
Payload: payload,
Timestamp: time.Now(),
}
messageBytes := h.messageToBytes(message)
for _, conn := range h.connections {
if conn.UserID == userID {
select {
case conn.send <- messageBytes:
default:
}
}
}
}
// WebSocket HTTP Handler
type WebSocketHandler struct {
hub *WebSocketHub
}
func NewWebSocketHandler(hub *WebSocketHub) *WebSocketHandler {
return &WebSocketHandler{
hub: hub,
}
}
func (h *WebSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// In real implementation, upgrade to WebSocket connection
connID := fmt.Sprintf("conn-%d", time.Now().UnixNano())
userID := r.URL.Query().Get("user_id")
conn := &WebSocketConnection{
ID: connID,
UserID: userID,
send: make(chan []byte, 256),
hub: h.hub,
}
h.hub.register <- conn
// Simulate connection
fmt.Fprintf(w, "WebSocket connection established: %s\n", connID)
}
API Versioning Strategies
// API Versioning Implementation
package main
import (
"fmt"
"net/http"
"strings"
)
type APIVersion string
const (
V1 APIVersion = "v1"
V2 APIVersion = "v2"
V3 APIVersion = "v3"
)
// Version Router
type VersionRouter struct {
handlers map[APIVersion]http.Handler
}
func NewVersionRouter() *VersionRouter {
return &VersionRouter{
handlers: make(map[APIVersion]http.Handler),
}
}
func (vr *VersionRouter) Register(version APIVersion, handler http.Handler) {
vr.handlers[version] = handler
fmt.Printf("📌 Registered API version: %s\n", version)
}
func (vr *VersionRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Strategy 1: URL Path versioning (/api/v1/users)
version := vr.extractVersionFromPath(r.URL.Path)
// Strategy 2: Header versioning (Accept: application/vnd.api.v1+json)
if version == "" {
version = vr.extractVersionFromHeader(r.Header.Get("Accept"))
}
// Strategy 3: Query parameter (?version=v1)
if version == "" {
version = APIVersion(r.URL.Query().Get("version"))
}
// Default to latest version
if version == "" {
version = V3
}
handler, exists := vr.handlers[version]
if !exists {
http.Error(w, fmt.Sprintf("API version %s not supported", version), http.StatusNotFound)
return
}
// Add version header to response
w.Header().Set("X-API-Version", string(version))
handler.ServeHTTP(w, r)
}
func (vr *VersionRouter) extractVersionFromPath(path string) APIVersion {
parts := strings.Split(strings.Trim(path, "/"), "/")
for _, part := range parts {
if strings.HasPrefix(part, "v") && len(part) == 2 {
return APIVersion(part)
}
}
return ""
}
func (vr *VersionRouter) extractVersionFromHeader(accept string) APIVersion {
// Parse Accept header like: application/vnd.api.v1+json
if strings.Contains(accept, "vnd.api.") {
parts := strings.Split(accept, ".")
for _, part := range parts {
if strings.HasPrefix(part, "v") {
version := strings.Split(part, "+")[0]
return APIVersion(version)
}
}
}
return ""
}
API Documentation
// OpenAPI/Swagger Documentation Generator
package main
import (
"encoding/json"
"fmt"
)
type OpenAPISpec struct {
OpenAPI string `json:"openapi"`
Info OpenAPIInfo `json:"info"`
Servers []OpenAPIServer `json:"servers"`
Paths map[string]OpenAPIPath `json:"paths"`
}
type OpenAPIInfo struct {
Title string `json:"title"`
Description string `json:"description"`
Version string `json:"version"`
}
type OpenAPIServer struct {
URL string `json:"url"`
Description string `json:"description"`
}
type OpenAPIPath struct {
Get *OpenAPIOperation `json:"get,omitempty"`
Post *OpenAPIOperation `json:"post,omitempty"`
Put *OpenAPIOperation `json:"put,omitempty"`
Delete *OpenAPIOperation `json:"delete,omitempty"`
}
type OpenAPIOperation struct {
Summary string `json:"summary"`
Description string `json:"description"`
Tags []string `json:"tags"`
Parameters []OpenAPIParameter `json:"parameters,omitempty"`
RequestBody *OpenAPIRequestBody `json:"requestBody,omitempty"`
Responses map[string]OpenAPIResponse `json:"responses"`
}
type OpenAPIParameter struct {
Name string `json:"name"`
In string `json:"in"`
Description string `json:"description"`
Required bool `json:"required"`
Schema Schema `json:"schema"`
}
type OpenAPIRequestBody struct {
Description string `json:"description"`
Required bool `json:"required"`
Content map[string]MediaType `json:"content"`
}
type OpenAPIResponse struct {
Description string `json:"description"`
Content map[string]MediaType `json:"content,omitempty"`
}
type MediaType struct {
Schema Schema `json:"schema"`
}
type Schema struct {
Type string `json:"type"`
Properties map[string]Schema `json:"properties,omitempty"`
Items *Schema `json:"items,omitempty"`
}
func GenerateOpenAPISpec() *OpenAPISpec {
spec := &OpenAPISpec{
OpenAPI: "3.0.0",
Info: OpenAPIInfo{
Title: "User API",
Description: "RESTful API for user management",
Version: "1.0.0",
},
Servers: []OpenAPIServer{
{
URL: "http://localhost:8080/api/v1",
Description: "Development server",
},
},
Paths: make(map[string]OpenAPIPath),
}
// Define /users endpoint
spec.Paths["/users"] = OpenAPIPath{
Get: &OpenAPIOperation{
Summary: "List users",
Description: "Get a paginated list of users",
Tags: []string{"users"},
Parameters: []OpenAPIParameter{
{
Name: "page",
In: "query",
Description: "Page number",
Required: false,
Schema: Schema{Type: "integer"},
},
{
Name: "per_page",
In: "query",
Description: "Items per page",
Required: false,
Schema: Schema{Type: "integer"},
},
},
Responses: map[string]OpenAPIResponse{
"200": {
Description: "Successful response",
Content: map[string]MediaType{
"application/json": {
Schema: Schema{
Type: "object",
Properties: map[string]Schema{
"success": {Type: "boolean"},
"data": {
Type: "array",
Items: &Schema{Type: "object"},
},
},
},
},
},
},
},
},
Post: &OpenAPIOperation{
Summary: "Create user",
Description: "Create a new user",
Tags: []string{"users"},
RequestBody: &OpenAPIRequestBody{
Description: "User object",
Required: true,
Content: map[string]MediaType{
"application/json": {
Schema: Schema{
Type: "object",
Properties: map[string]Schema{
"username": {Type: "string"},
"email": {Type: "string"},
},
},
},
},
},
Responses: map[string]OpenAPIResponse{
"201": {
Description: "User created successfully",
},
},
},
}
return spec
}
func (spec *OpenAPISpec) ToJSON() string {
bytes, _ := json.MarshalIndent(spec, "", " ")
return string(bytes)
}
API Best Practices
1. Consistent Error Handling
// Standardized Error Responses
type StandardError struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
Timestamp time.Time `json:"timestamp"`
RequestID string `json:"request_id"`
}
func NewStandardError(code, message string) *StandardError {
return &StandardError{
Code: code,
Message: message,
Timestamp: time.Now(),
RequestID: generateRequestID(),
}
}
func generateRequestID() string {
return fmt.Sprintf("req-%d", time.Now().UnixNano())
}
2. Rate Limiting
// API Rate Limiter (covered in detail in rate limiting post)
type APIRateLimiter struct {
requests map[string][]time.Time
limit int
window time.Duration
mutex sync.RWMutex
}
func NewAPIRateLimiter(limit int, window time.Duration) *APIRateLimiter {
return &APIRateLimiter{
requests: make(map[string][]time.Time),
limit: limit,
window: window,
}
}
func (rl *APIRateLimiter) Allow(clientID string) bool {
rl.mutex.Lock()
defer rl.mutex.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
// Get client's request history
requests := rl.requests[clientID]
// Filter out old requests
valid := make([]time.Time, 0)
for _, reqTime := range requests {
if reqTime.After(cutoff) {
valid = append(valid, reqTime)
}
}
// Check if limit exceeded
if len(valid) >= rl.limit {
rl.requests[clientID] = valid
return false
}
// Add current request
valid = append(valid, now)
rl.requests[clientID] = valid
return true
}
3. API Security
// API Authentication Middleware
type AuthMiddleware struct {
secretKey string
}
func NewAuthMiddleware(secretKey string) *AuthMiddleware {
return &AuthMiddleware{
secretKey: secretKey,
}
}
func (am *AuthMiddleware) Authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract token from Authorization header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
// Validate token (simplified)
token := strings.TrimPrefix(authHeader, "Bearer ")
if !am.validateToken(token) {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func (am *AuthMiddleware) validateToken(token string) bool {
// Simplified token validation
return token != ""
}
API Comparison
| Feature | REST | GraphQL | gRPC | WebSocket |
|---|---|---|---|---|
| Protocol | HTTP | HTTP | HTTP/2 | WebSocket |
| Data Format | JSON | JSON | Protobuf | JSON/Binary |
| Paradigm | Resource-based | Query-based | RPC | Event-based |
| Performance | Good | Good | Excellent | Excellent |
| Real-time | No | Subscriptions | Streaming | Yes |
| Browser Support | Excellent | Excellent | Limited | Good |
| Caching | Easy | Complex | N/A | N/A |
| Best For | CRUD operations | Flexible queries | Microservices | Real-time apps |
Conclusion
Choose REST when:
- Building public APIs
- Simple CRUD operations
- Need caching
- Wide client support required
Choose GraphQL when:
- Clients need flexible queries
- Mobile/bandwidth constraints
- Rapid frontend iteration
- Complex data relationships
Choose gRPC when:
- Microservices communication
- Performance critical
- Type safety important
- Streaming required
Choose WebSocket when:
- Real-time bidirectional communication
- Live updates/notifications
- Chat/collaboration features
- Game development
Design APIs with consistency, documentation, versioning, and security in mind from day one.