REST API: The Architectural Style for Web Services

Why This Matters

In 2000, Roy Fielding introduced REST in his PhD dissertation, and it revolutionized how we build web services. Today, REST powers the majority of public APIs—from Twitter and GitHub to Stripe and Twilio. It's the de facto standard for building web services that scale.
But here's the truth most developers miss: REST isn't a protocol or a standard—it's an architectural style. It's a set of constraints that, when followed correctly, create APIs that are:
  • Scalable: Stateless design allows horizontal scaling
  • Cacheable: HTTP caching reduces server load by 70%+
  • Discoverable: HATEOAS makes APIs self-documenting
  • Simple: Uses standard HTTP methods everyone understands
Yet most "REST APIs" violate fundamental REST principles. They're really just "HTTP APIs with JSON." Understanding true REST constraints separates senior engineers from junior developers who just copy Stack Overflow examples.
By the end of this deep-dive, you'll understand:
  • The six REST constraints (spoiler: most APIs only follow 3)
  • How HTTP methods map to CRUD operations correctly
  • When to use status codes 200 vs 201 vs 204 vs 409
  • Why HATEOAS matters (and why most APIs ignore it)
Real-world impact: GitHub's REST API handles 5 billion requests per day. Stripe's API processes $640 billion in payments annually. Understanding REST design is critical for building production-grade systems.

What REST Solves

Pre-REST World: SOAP and RPC Chaos

Before REST, we had SOAP (Simple Object Access Protocol):
xml
<!-- SOAP Request: Verbose, complex --> <?xml version="1.0"?> <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"> <soap:Header> <auth:Authentication xmlns:auth="http://example.com/auth"> <auth:Username>user</auth:Username> <auth:Password>pass</auth:Password> </auth:Authentication> </soap:Header> <soap:Body> <m:GetUser xmlns:m="http://example.com/users"> <m:UserId>123</m:UserId> </m:GetUser> </soap:Body> </soap:Envelope>
Problems with SOAP:
  1. Complexity: XML parsing, WSDL schemas, WS-* specifications
  2. Verbosity: 10x larger payloads than REST/JSON
  3. Tight coupling: Client needs WSDL to generate code
  4. No caching: Everything is POST, can't leverage HTTP caching
  5. Tooling dependency: Requires special libraries (Apache Axis, etc.)

Problems REST Addresses

1. Scalability Through Statelessness
Traditional web apps stored session state on the server:
User → Server A (session stored) → Must always route to Server A
This prevents horizontal scaling. REST enforces statelessness:
User → Any Server (no session state) → Can route to any server
2. Network Efficiency Through Caching
Without caching, every request hits the database:
Client: "Get user 123" Server: Query database, return data Client: "Get user 123" (again, 1 second later) Server: Query database again (waste!)
REST with HTTP caching:
Client: "Get user 123" Server: Return data + Cache-Control: max-age=300 Client: "Get user 123" (within 5 minutes) Client cache: Return cached data (no server request!)
3. Uniform Interface Reduces Complexity
Pre-REST APIs had arbitrary endpoints:
/getUserById?id=123 /deleteUserAccount /updateUserEmail /getUsersByDateRange?start=...&end=...
REST uses uniform HTTP methods:
GET /users/123 (get user) DELETE /users/123 (delete user) PATCH /users/123 (update user) GET /users?since=... (list users)

Real-World Scenarios Where REST Excels

  1. Public APIs: GitHub, Stripe, Twilio (developer-friendly)
  2. CRUD Applications: Most web apps (user management, e-commerce)
  3. Mobile Apps: Efficient bandwidth usage with HTTP caching
  4. Microservices: Simple HTTP-based service communication
  5. Third-Party Integrations: Standard HTTP makes integration easy

REST Architectural Constraints

REST defines six constraints that create scalable, maintainable APIs:

1. Client-Server Architecture

Separation of concerns: Client handles UI, server handles data/business logic.
Flow diagram showing process

Flow diagram showing process

Benefits: Client and server can evolve independently.

2. Statelessness

No session state on server. Each request contains all necessary information.
Request 1

Request 1

Benefits:
  • Horizontal scaling (any server can handle any request)
  • No session synchronization needed
  • Simpler server logic

3. Cacheability

Responses must define if they're cacheable.
go
// Server sets cache headers w.Header().Set("Cache-Control", "public, max-age=300") // 5 minutes w.Header().Set("ETag", `"v1.2.3"`) // Client can cache response // Subsequent requests within 5 minutes use cached data
Benefits:
  • Reduces server load by 50-90%
  • Faster response times
  • Lower bandwidth costs

4. Layered System

Client doesn't know if connected directly to server or through intermediaries.
Flow diagram showing process

Flow diagram showing process

Benefits:
  • Add caching layers without client changes
  • Security layers (firewalls, rate limiting)
  • Easy infrastructure changes

5. Code on Demand (Optional)

Server can send executable code to client (JavaScript).
This is the only optional constraint. Rarely used in modern APIs.

6. Uniform Interface

The most important constraint. Four sub-constraints:
a) Resource-Based: URIs identify resources
/users/123 (resource: user with ID 123) /users/123/orders (resource: orders for user 123)
b) Manipulation Through Representations: JSON/XML represent resource state
json
GET /users/123{"id": 123, "name": "Alice", "email": "alice@example.com"}
c) Self-Descriptive Messages: Each message contains enough info to process it
http
GET /users/123 HTTP/1.1 Host: api.example.com Accept: application/json Authorization: Bearer token123
d) HATEOAS: Hypermedia As The Engine Of Application State
json
{ "id": 123, "name": "Alice", "links": { "self": "/users/123", "orders": "/users/123/orders", "delete": "/users/123" } }

Resources as Nouns, Methods as Verbs

Think of REST like working with a filing cabinet:
Filing Cabinet = Database
  • Drawers = Collections (/users, /orders)
  • Folders = Individual Resources (/users/123)
  • Documents = Representations (JSON, XML)
HTTP Methods = Actions:
  • GET: Read a document (doesn't change anything)
  • POST: Add a new document to a drawer
  • PUT: Replace an entire document
  • PATCH: Update part of a document
  • DELETE: Remove a document
Bad (RPC-style):
POST /createUser POST /deleteUser POST /updateUserEmail
Good (REST-style):
POST /users (create) DELETE /users/123 (delete) PATCH /users/123 (update)

Deep Technical Dive

1. HTTP Methods: Correct Usage

Flow diagram showing process

Flow diagram showing process

2. Code Example: Production REST API in Go

go
package main import ( "encoding/json" "fmt" "log" "net/http" "strconv" "sync" "time" "github.com/gorilla/mux" ) // User represents a user resource type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` CreatedAt time.Time `json:"created_at"` } // UserStore manages user data type UserStore struct { users map[int]*User nextID int mu sync.RWMutex } func NewUserStore() *UserStore { return &UserStore{ users: make(map[int]*User), nextID: 1, } } // HTTP Handlers // GET /users - List all users func (s *UserStore) ListUsers(w http.ResponseWriter, r *http.Request) { s.mu.RLock() defer s.mu.RUnlock() users := make([]*User, 0, len(s.users)) for _, user := range s.users { users = append(users, user) } // Set cache headers (cacheable for 5 minutes) w.Header().Set("Cache-Control", "public, max-age=300") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(users) } // GET /users/{id} - Get specific user func (s *UserStore) GetUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } s.mu.RLock() user, exists := s.users[id] s.mu.RUnlock() if !exists { http.Error(w, "User not found", http.StatusNotFound) return } // Set ETag for conditional requests etag := fmt.Sprintf(`"user-%d-v1"`, id) w.Header().Set("ETag", etag) w.Header().Set("Cache-Control", "public, max-age=300") // Check If-None-Match header (client cache validation) if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } // POST /users - Create new user func (s *UserStore) CreateUser(w http.ResponseWriter, r *http.Request) { var input struct { Name string `json:"name"` Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Validation if input.Name == "" || input.Email == "" { http.Error(w, "Name and email are required", http.StatusBadRequest) return } s.mu.Lock() user := &User{ ID: s.nextID, Name: input.Name, Email: input.Email, CreatedAt: time.Now(), } s.users[s.nextID] = user s.nextID++ s.mu.Unlock() // Set Location header to new resource w.Header().Set("Location", fmt.Sprintf("/users/%d", user.ID)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) // 201 Created json.NewEncoder(w).Encode(user) } // PUT /users/{id} - Replace entire user func (s *UserStore) ReplaceUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } var input struct { Name string `json:"name"` Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } s.mu.Lock() defer s.mu.Unlock() // PUT creates resource if doesn't exist (idempotent) user, exists := s.users[id] if !exists { user = &User{ID: id, CreatedAt: time.Now()} s.users[id] = user } // Replace entire resource user.Name = input.Name user.Email = input.Email w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } // PATCH /users/{id} - Partial update func (s *UserStore) UpdateUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } s.mu.Lock() defer s.mu.Unlock() user, exists := s.users[id] if !exists { http.Error(w, "User not found", http.StatusNotFound) return } // Partial update (only provided fields) var updates map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } if name, ok := updates["name"].(string); ok { user.Name = name } if email, ok := updates["email"].(string); ok { user.Email = email } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } // DELETE /users/{id} - Delete user func (s *UserStore) DeleteUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } s.mu.Lock() defer s.mu.Unlock() if _, exists := s.users[id]; !exists { http.Error(w, "User not found", http.StatusNotFound) return } delete(s.users, id) // 204 No Content (successful deletion, no body) w.WriteHeader(http.StatusNoContent) } // OPTIONS /users - CORS preflight func (s *UserStore) OptionsUsers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Allow", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.WriteHeader(http.StatusOK) } func main() { store := NewUserStore() router := mux.NewRouter() // REST endpoints router.HandleFunc("/users", store.ListUsers).Methods("GET") router.HandleFunc("/users", store.CreateUser).Methods("POST") router.HandleFunc("/users", store.OptionsUsers).Methods("OPTIONS") router.HandleFunc("/users/{id}", store.GetUser).Methods("GET") router.HandleFunc("/users/{id}", store.ReplaceUser).Methods("PUT") router.HandleFunc("/users/{id}", store.UpdateUser).Methods("PATCH") router.HandleFunc("/users/{id}", store.DeleteUser).Methods("DELETE") // Middleware for logging router.Use(loggingMiddleware) log.Println("REST API server starting on :8080") log.Fatal(http.ListenAndServe(":8080", router)) } func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) }

3. HTTP Status Codes: When to Use Each

go
// 2xx Success w.WriteHeader(http.StatusOK) // 200 - GET, PUT, PATCH success w.WriteHeader(http.StatusCreated) // 201 - POST created resource w.WriteHeader(http.StatusNoContent) // 204 - DELETE success, no body // 3xx Redirection w.WriteHeader(http.StatusNotModified) // 304 - Resource not modified (ETag match) w.WriteHeader(http.StatusPermanentRedirect) // 308 - Resource moved permanently // 4xx Client Errors w.WriteHeader(http.StatusBadRequest) // 400 - Invalid request format w.WriteHeader(http.StatusUnauthorized) // 401 - Missing/invalid authentication w.WriteHeader(http.StatusForbidden) // 403 - Authenticated but not authorized w.WriteHeader(http.StatusNotFound) // 404 - Resource doesn't exist w.WriteHeader(http.StatusConflict) // 409 - Resource conflict (duplicate) w.WriteHeader(http.StatusUnprocessableEntity) // 422 - Validation failed w.WriteHeader(http.StatusTooManyRequests) // 429 - Rate limit exceeded // 5xx Server Errors w.WriteHeader(http.StatusInternalServerError) // 500 - Server error w.WriteHeader(http.StatusServiceUnavailable) // 503 - Temporary unavailable

4. Code Example: API Versioning Strategies

go
// Strategy 1: URI Versioning (most common) router.HandleFunc("/api/v1/users", handleUsersV1) router.HandleFunc("/api/v2/users", handleUsersV2) // Strategy 2: Header Versioning func handleUsers(w http.ResponseWriter, r *http.Request) { version := r.Header.Get("API-Version") switch version { case "2": handleUsersV2(w, r) default: handleUsersV1(w, r) } } // Strategy 3: Content Negotiation // Accept: application/vnd.myapi.v2+json func handleUsers(w http.ResponseWriter, r *http.Request) { accept := r.Header.Get("Accept") if strings.Contains(accept, "v2+json") { handleUsersV2(w, r) } else { handleUsersV1(w, r) } } // Strategy 4: Query Parameter (not recommended) // GET /users?version=2

5. Code Example: HATEOAS Implementation

go
type UserResponse struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` Links Links `json:"_links"` } type Links struct { Self Link `json:"self"` Orders Link `json:"orders,omitempty"` Delete Link `json:"delete,omitempty"` } type Link struct { Href string `json:"href"` Method string `json:"method,omitempty"` } func (s *UserStore) GetUserWithHATEOAS(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, _ := strconv.Atoi(vars["id"]) s.mu.RLock() user, exists := s.users[id] s.mu.RUnlock() if !exists { http.Error(w, "User not found", http.StatusNotFound) return } // Build response with hypermedia links response := UserResponse{ ID: user.ID, Name: user.Name, Email: user.Email, Links: Links{ Self: Link{ Href: fmt.Sprintf("/users/%d", id), Method: "GET", }, Orders: Link{ Href: fmt.Sprintf("/users/%d/orders", id), Method: "GET", }, Delete: Link{ Href: fmt.Sprintf("/users/%d", id), Method: "DELETE", }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // Client discovers available actions from response // No need to hardcode URLs in client

When to Use: Decision Framework

Flow diagram showing process

Flow diagram showing process

Use REST When:

  • Building public APIs for third-party developers
  • CRUD operations on resources
  • Need standard HTTP caching
  • Want wide client compatibility
  • Simplicity and familiarity important
  • Examples: GitHub API, Stripe API, Twilio API

Use GraphQL When:

  • Clients need flexible querying
  • Multiple client types with different data needs
  • Want to reduce over-fetching/under-fetching
  • Building BFF (Backend for Frontend)
  • Examples: Facebook, GitHub (also has GraphQL), Shopify

Use gRPC When:

  • Internal microservices communication
  • High-performance requirements
  • Strong typing needed (Protocol Buffers)
  • Streaming required
  • Examples: Netflix microservices, Uber backend

Use SOAP When:

  • Legacy enterprise systems
  • WS-Security requirements
  • ACID transaction support needed
  • Examples: Banking systems, government services

Common Pitfalls and How to Avoid Them

Pitfall 1: Treating REST as "HTTP with JSON"

The Mistake:
go
// POST for everything (not REST!) POST /getUserById POST /deleteUser POST /updateUser
The Fix:
go
// Use proper HTTP methods GET /users/123 DELETE /users/123 PATCH /users/123

Pitfall 2: Non-Idempotent PUT/DELETE

The Mistake:
go
// PUT increments counter (NOT IDEMPOTENT!) func UpdateUser(w http.ResponseWriter, r *http.Request) { user.LoginCount++ // Side effect! }
The Fix:
go
// PUT replaces entire resource (idempotent) func ReplaceUser(w http.ResponseWriter, r *http.Request) { user.Name = input.Name user.Email = input.Email // No side effects, same result every time }

Pitfall 3: Wrong Status Codes

The Mistake:
go
// Always returning 200, even for errors w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "error": "User not found", // WRONG! })
The Fix:
go
// Use proper status codes if !exists { http.Error(w, "User not found", http.StatusNotFound) // 404 return }

Pitfall 4: Storing State on Server

The Mistake:
go
// Storing user session in server memory sessions[sessionID] = userData // VIOLATES STATELESSNESS
The Fix:
go
// Use JWT or similar stateless auth token := jwt.GenerateToken(userID) // Client sends token with each request // Server validates token without storing session

Pitfall 5: Not Using HTTP Caching

The Mistake:
go
// No cache headers w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) // Client can't cache!
The Fix:
go
// Add cache headers w.Header().Set("Cache-Control", "public, max-age=300") w.Header().Set("ETag", `"v1.2.3"`) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user)

Pitfall 6: Exposing Database Schema Directly

The Mistake:
go
// Exposing database IDs and structure type User struct { InternalID int `json:"internal_id"` // Leaks DB structure PasswordHash string `json:"password_hash"` // SECURITY ISSUE! DeletedAt *time.Time `json:"deleted_at"` // Implementation detail }
The Fix:
go
// DTO (Data Transfer Object) pattern type UserResponse struct { ID string `json:"id"` // UUID, not DB ID Name string `json:"name"` Email string `json:"email"` // Only expose what clients need }

Performance Implications

REST Performance Characteristics

HTTP/1.1 Overhead:
  • Request headers: 400-800 bytes
  • Response headers: 200-400 bytes
  • Per-request TCP/TLS handshake (if not keep-alive)
HTTP/2 Benefits:
  • Header compression (HPACK)
  • Multiplexing (multiple requests over one connection)
  • Server push (can push related resources)

Optimization Strategies

1. HTTP Caching:
go
// Client-side caching reduces requests by 70-90% w.Header().Set("Cache-Control", "public, max-age=3600") w.Header().Set("ETag", generateETag(data)) // Conditional requests (If-None-Match) if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) return }
2. Compression:
go
// Enable gzip compression (reduces payload by 70-90%) import "github.com/gorilla/handlers" router.Use(handlers.CompressHandler)
3. Pagination:
go
// Don't return all records GET /users?page=1&limit=50 // Cursor-based pagination for large datasets GET /users?cursor=abc123&limit=50
4. Field Selection:
go
// Allow clients to request specific fields GET /users?fields=id,name,email // Reduces payload size and database queries
5. Connection Pooling:
go
// Reuse HTTP connections client := &http.Client{ Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, }, }

Testing REST APIs

Unit Testing with httptest

go
import ( "net/http/httptest" "testing" ) func TestGetUser(t *testing.T) { store := NewUserStore() store.users[1] = &User{ID: 1, Name: "Alice", Email: "alice@example.com"} req := httptest.NewRequest("GET", "/users/1", nil) w := httptest.NewRecorder() router := mux.NewRouter() router.HandleFunc("/users/{id}", store.GetUser) router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected 200, got %d", w.Code) } var user User json.NewDecoder(w.Body).Decode(&user) if user.Name != "Alice" { t.Errorf("Expected Alice, got %s", user.Name) } }

Integration Testing with Real HTTP

go
func TestAPIIntegration(t *testing.T) { // Start test server server := httptest.NewServer(setupRouter()) defer server.Close() // Create user resp, _ := http.Post(server.URL+"/users", "application/json", strings.NewReader(`{"name":"Bob","email":"bob@example.com"}`)) if resp.StatusCode != http.StatusCreated { t.Fatalf("Expected 201, got %d", resp.StatusCode) } // Get location header location := resp.Header.Get("Location") // Fetch created user resp, _ = http.Get(server.URL + location) if resp.StatusCode != http.StatusOK { t.Fatalf("Expected 200, got %d", resp.StatusCode) } }

Real-World Use Cases

Use Case 1: GitHub REST API

GitHub's API handles 5+ billion requests per day:
Design Principles:
  • Versioned via URI (/api/v3/)
  • Rate limiting (5000 req/hour authenticated)
  • Pagination with Link headers
  • HATEOAS (responses include action URLs)
Example:
bash
GET /repos/golang/go/issues Link: <https://api.github.com/repos/golang/go/issues?page=2>; rel="next"

Use Case 2: Stripe Payment API

Stripe processes $640 billion annually through their REST API:
Design Principles:
  • Idempotency keys for safe retries
  • Versioned via headers (Stripe-Version: 2023-10-16)
  • Webhook events for async operations
  • Expandable resources (reduce round trips)
Example:
bash
POST /v1/charges Idempotency-Key: unique-key-123

Use Case 3: Twitter API v2

Design Principles:
  • OAuth 2.0 authentication
  • Field selection to reduce payload
  • Real-time streaming endpoints
  • Comprehensive error responses with error codes

Interview Questions: What You Should Know

Junior Level

Q: What does REST stand for and what is it? A: REST stands for Representational State Transfer. It's an architectural style (not a protocol) that uses HTTP methods, stateless communication, and resource-based URIs to build scalable web services.
Q: What are the main HTTP methods and what do they do? A:
  • GET: Retrieve resource (safe, idempotent)
  • POST: Create resource (not idempotent)
  • PUT: Replace entire resource (idempotent)
  • PATCH: Partial update (can be idempotent)
  • DELETE: Remove resource (idempotent)
Q: What's the difference between PUT and PATCH? A: PUT replaces the entire resource (must send all fields). PATCH updates only specified fields (partial update).

Mid Level

Q: Explain the six REST constraints. A:
  1. Client-Server: Separation of concerns
  2. Stateless: No session state on server
  3. Cacheable: Responses must define cacheability
  4. Layered System: Client doesn't know intermediaries
  5. Code on Demand: (Optional) Server can send executable code
  6. Uniform Interface: Resource-based, self-descriptive messages, HATEOAS
Q: What makes an HTTP method idempotent and why does it matter? A: Idempotent means calling it multiple times has the same effect as calling it once. GET, PUT, DELETE are idempotent. POST is not. This matters for retry logic—idempotent operations can be safely retried without side effects.
Q: How would you implement API versioning? A: Four strategies:
  1. URI versioning: /api/v1/users (most common, explicit)
  2. Header versioning: API-Version: 2 (cleaner URIs)
  3. Content negotiation:
    Accept: application/vnd.api.v2+json
  4. Query parameter: ?version=2 (not recommended)

Senior Level

Q: Design a RESTful API for a blog platform with posts, comments, and users. A:
Resources: GET /users - List users POST /users - Create user GET /users/{id} - Get user PATCH /users/{id} - Update user DELETE /users/{id} - Delete user GET /posts - List posts POST /posts - Create post GET /posts/{id} - Get post PATCH /posts/{id} - Update post DELETE /posts/{id} - Delete post GET /posts/{id}/comments - List comments for post POST /posts/{id}/comments - Create comment on post GET /comments/{id} - Get specific comment DELETE /comments/{id} - Delete comment Considerations: - Pagination for lists - Filtering: GET /posts?author={userId}&published=true - Sorting: GET /posts?sort=-created_at (descending) - Field selection: GET /posts?fields=title,summary - Authentication: JWT tokens - Rate limiting: 1000 req/hour per user - Caching: ETag for conditional requests
Q: How would you handle API authentication and authorization in REST? A:
  1. Authentication (Who are you?):
    • JWT tokens in Authorization header
    • OAuth 2.0 for third-party access
    • API keys for service-to-service
  2. Authorization (What can you do?):
    • RBAC (Role-Based Access Control)
    • Resource-level permissions
    • HTTP 401 for auth, 403 for authz
  3. Implementation:
go
func authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") userID, err := validateJWT(token) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Add user to context ctx := context.WithValue(r.Context(), "userID", userID) next.ServeHTTP(w, r.WithContext(ctx)) }) }
Q: How would you handle long-running operations in REST? A: Use async processing with 202 Accepted:
POST /videos → 202 Accepted Location: /videos/123/status GET /videos/123/status → 200 OK {"status": "processing", "progress": 45} GET /videos/123/status (later) → 303 See Other Location: /videos/123

Key Takeaways

  1. REST is an architectural style, not a protocol - six constraints define it
  2. Statelessness enables horizontal scaling - no session state on server
  3. HTTP caching reduces load by 70%+ - use Cache-Control and ETag
  4. Use correct HTTP methods - GET (read), POST (create), PUT (replace), PATCH (update), DELETE (remove)
  5. Status codes matter - 200 vs 201 vs 204 vs 404 vs 409
  6. Idempotency is critical - GET, PUT, DELETE are idempotent; POST is not
  7. HATEOAS makes APIs discoverable - include hypermedia links in responses
  8. Versioning prevents breaking changes - URI versioning most common
  9. Authentication via tokens - JWT or OAuth 2.0, not sessions
  10. REST excels for CRUD - but consider GraphQL/gRPC for other patterns

Further Learning

  1. Practice: Build a full REST API with authentication and caching
  2. Read: Roy Fielding's dissertation on REST (original source)
  3. Study: GitHub and Stripe API documentation (excellent examples)
  4. Tools: Learn Postman, curl, and OpenAPI/Swagger
  5. Advanced: Study Richardson Maturity Model (levels 0-3 of REST)
REST isn't just about HTTP endpoints—it's a philosophy for building scalable, maintainable, and cacheable web services. Master these principles, and you'll design APIs that developers love to use.
All Blogs
Tags:resthttpapi-designnetworkinginterview-prep