
A REST API (Representational State Transfer Application Programming Interface) is a set of rules for designing web services that allows different software applications to communicate with each other over the internet using standard HTTP requests. It is a widely adopted architectural style for building scalable, lightweight, and efficient web services.
REST is not a protocol. This is the first trap developers fall into. HTTP is a protocol: a strict set of rules for communication. REST is an architectural style, a set of constraints and principles for designing networked applications. You can build a RESTful API over HTTP, but you can also violate every REST constraint while still using HTTP methods correctly.
The 6 Constraints
REST defines six architectural constraints. Violate them, and you're building an HTTP API. Respect them, and you're building a system that inherits the web's superpowers: scalability, reliability, and evolvability.
1. Client-Server Architecture
The client handles presentation and the server handles data storage. This allows both to evolve independently. Your mobile app can ship new UI features without touching the backend. Your backend can migrate databases without breaking clients.
2. Statelessness
Each request from client to server must contain all information needed to understand and process it. The server stores no client context between requests. No session state on the server means any server can handle any request. Horizontal scaling becomes trivial. But this would mean more data over the request (authentication tokens in every header), and the client must manage application state.
3. Cacheability
Responses must explicitly define themselves as cacheable or not. This eliminates redundant requests, reducing server load and latency. A GET /users/123 that returns Cache-Control: max-age=3600 means intermediaries and clients can serve that data for an hour without hitting your server.
4. Uniform Interface
This is the most fundamental rule. It requires a standardized way of interacting with the server, regardless of the device. It is further broken down into:
-
Resource Identification: Resources are identified by URIs (/orders/456), not by operations (/getOrder?id=456).
-
Resource Manipulation through Representations: Clients modify resources by sending representations (like JSON).
-
Self-Descriptive Messages: Each message contains enough information to describe how to process it (Content-Type: application/json, Accept: application/xml).
-
Hypermedia as the Engine of Application State (HATEOAS): Responses contain links to related actions. A GET /orders/456 returns the order plus links to cancel, pay, or track. The client discovers actions dynamically rather than hardcoding URLs.
Example:
{
"orderId": 456,
"status": "AWAITING_PAYMENT",
"total": 50.00,
"_links": {
"self": { "href": "/orders/456" },
"pay": { "href": "/orders/456/payments", "method": "POST" },
"cancel": { "href": "/orders/456", "method": "DELETE" }
}
}
Despite being a "core rule" of REST, HATEOAS is rarely used in the real world. In fact, most "REST APIs" you use (Stripe, GitHub, Twilio) are actually just HTTP APIs because they skip this part.
5. Layered System
A client cannot ordinarily tell whether it is connected directly to the end server or to an intermediary (load balancer, cache, proxy). This enables load balancing, shared caches, and security layers without client awareness.
6. Code on Demand (Optional)
Servers can temporarily extend client functionality by transferring executable code (JavaScript applets). Rarely used in modern APIs, but historically enabled web browsers to download JavaScript.
REST vs. RESTful: The Critical Distinction
| REST | RESTful |
|---|---|
| The architectural style itself, the philosophy and constraints | An API that attempts to follow REST constraints |
| Abstract; exists as a concept | Concrete; exists as an implementation |
| You cannot "build" REST; you design with it | You build a RESTful API (or try to) |
| RPC over HTTP can use REST principles | True RESTful APIs use hypermedia and uniform interfaces |
Most "RESTful" APIs are actually HTTP APIs with REST-like characteristics. They use proper HTTP methods and status codes but ignore HATEOAS, violate statelessness with server-side sessions, or embed actions in URIs (/users/123/activate instead of using hypermedia controls). This isn't necessarily wrong but understanding the difference makes you intentional about your tradeoffs.
Your API doesn't need to be pure REST. But it should be intentionally impure, violating constraints for specific reasons, not from ignorance.
HTTP Deep Dive: Methods, Status Codes, and Headers
You can't build solid RESTful APIs without mastering the protocol they ride on. HTTP isn't just the delivery truck, it's the contract between your server and the world. Every method choice signals intent. Every status code communicates outcome. Every header negotiates capabilities.
Misuse them and you create APIs that confuse clients, break caches, and fail silently in production. Master them and you get idempotency guarantees, cache efficiency, and client libraries that "just work."
→ Read the full deep dive:
Understanding How the Web Actually Works (HTTP Explained Simply)
I used APIs every day without truly understanding what was happening under the hood. In this post, I break down HTTP, requests, responses, and how the web actually works, in a way that finally made things click for me.

We'll pick up with how to apply these HTTP fundamentals to resource design and URI patterns next.
REST API Design
By far, this is the most important secton on this blog. There are quite a lot of things we need to consider when designing an API. Here is the list:
- The mindset
- Resource Design (The Core)
- HTTP Methods (The Semantics)
- Status Codes (The Precisions)
- Request & Response Design
- Filtering, Sorting & Pagination
- Versioning
- Error Handling
- Authentication & Authorization
- Idempotency
- Rate Limiting
- Caching
- HATEOAS
- Content Negotiation
- API Documentation
- Testing Strategy
- Performance & Optimization
- Webhooks/Callbacks
- API Security
The Right Mindset
When designing a REST API, the most important thing is your mindset. Instead of thinking about actions like “create user” or “get orders,” you should think about the actual things in your system like users, orders, or applications. These are called resources. For example, instead of creating an endpoint like /getUser, you would simply have /users/, and use HTTP methods like **GET ** or POST to define what you want to do. This approach makes your API feel more natural and easier to understand. It also helps if your API reflects real-world relationships, like a user having many orders, which could be represented as /users//orders. The goal is to keep things simple, consistent, and predictable so that anyone using your API can quickly understand it without confusion.
Resource Design (The Core)
A resource is anything that can be named, a user, an order, a session, a search result, or a configuration. It is not your database table, though they often map, because resources are abstract concepts. It is not your ORM model because the API resource may combine multiple models into one cohesive representation. Most importantly, a resource is stable over time, the URI identifies the concept itself, not any particular representation. An order resource might combine data from orders, order_items, payments, and shipments tables, yet the client sees one unified resource while the backend handles the aggregation.
Resource naming
Never use verbs in URIs. Verbs define actions. When creating a resource, it is better to use noun.
| Wrong (RPC-style) | Right (Resource-style) |
|---|---|
POST /createUser | POST /users |
POST /getOrder | GET /orders/456 |
POST /updateProduct | PUT /products/789 |
POST /deleteComment | DELETE /comments/321 |
Convention: Plural vs Singular
Always use plural nouns for collection resources.
E.g:
/usersimplies a collection (GET returns list, POST creates new)/users/123implies a member of that collection- Consistency: every resource follows the same pattern
| Wrong | Right |
|---|---|
/user | /users |
/order | /orders |
/product | /products |
Resource Hierarchy: To Nest or Not to Nest
When resources related to each other, we will have to express them in a URI in a nested format. We are looking at three relation types:
| Type | URI Pattern | Example |
|---|---|---|
| Parent-child (dependent) | /parents/{id}/children | /users/123/orders |
| Independent association | /resource-a/{id}/resource-b/{id} | Rare, usually query instead |
| Many-to-many via junction | /junction-resources | /order-items |
Nesting rules:
- Nest only when the child cannot exist without the parent.
- Limit nesting depths to 2 levels
URI Design Checklist
Before finalizing your resource structure:
- All URIs use plural nouns (/orders not /order)
- No verbs in paths (/orders not /getOrders)
- Lowercase with hyphens (/order-items not /orderItems)
- Hierarchical only for dependent children
- Maximum 2 levels of nesting
- Surrogate keys for canonical URIs
- Consistent versioning (/v1/ everywhere)
- No file extensions (use Accept header)
- Trailing slash policy enforced (301 to canonical)
- Query params for filtering, not new endpoints
HTTP Methods & Status Codes
REST APIs speak through HTTP. The methods declare intent; the status codes report outcomes. Here is a quick reference on Status + Method combination
| Operation | Method | Success | Error |
|---|---|---|---|
| List resources | GET /users | 200 OK + array | 400 (bad params), 401, 403 |
| Get one resource | GET /users/123 | 200 OK + object | 404 (not found), 401, 403 |
| Create resource | POST /users | 201 Created + object + Location | 400, 422 (validation), 409 (conflict) |
| Full update | PUT /users/123 | 200 OK or 204 No Content | 400, 404, 409 (version conflict) |
| Partial update | PATCH /users/123 | 200 OK or 204 No Content | 400, 422, 404 |
| Delete resource | DELETE /users/123 | 204 No Content | 404, 409 (can't delete) |
-> Read more about HTTP in depth here:
Understanding How the Web Actually Works (HTTP Explained Simply)
I used APIs every day without truly understanding what was happening under the hood. In this post, I break down HTTP, requests, responses, and how the web actually works, in a way that finally made things click for me.

Request & Response Design
Once your resources and endpoints are defined, the next step is designing how data is sent to and from your API. This is where request and response design becomes important, because it directly affects how easy your API is to use and understand.
Request Design: When designing request, you need to keep certain things in mind:
- Use URL for resource identification → /users/123
- Use query params for filtering/sorting/pagination → /users?page=1&limit=10
- Use body for sending data (POST, PATCH, PUT)
- Keep requests minimal (only required fields)
- Validate all inputs on the server
- Allow partial updates (PATCH) instead of forcing full objects
Response Design:
- Keep a consistent structure
- Separate:
- data → actual result
- meta/pagination → extra info
- error → issues
- Use proper HTTP status codes
For single resource:
{
"data": { "id": 1, "name": "Harry" }
}For list:
{
"data": [...],
"pagination": { "page": 1, "total": 100 }
}For errors:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found"
}
}Filtering, Sorting & Pagination
When your API starts returning lists of data (users, jobs, expenses, etc.), you need a way to control what data is returned and how much of it. That’s where filtering, sorting, and pagination come in. A good design here makes your API scalable and easy to use, while a bad one quickly becomes messy and inefficient.
Filtering
Instead of receiving all orders, your frontend request only specific results based on certain conditions.
GET /orders?status=pending&customer_id=456&created_after=2024-01-01T00:00:00ZUse query parameters because they are cacheable and optional.
Validation rules
- Reject unknown query parameters with
400 Bad Requestrather than silently ignoring them this catches typos like ?stauts=pending immediately. - Treat empty values as "no filter," not as "filter by empty string" unless empty string is semantically meaningful.
Sorting
Sorting controls presentation order. Without explicit sorting, databases return records in undefined order, typically creation sequence, but never guaranteed.
GET /orders?sort=-created_at,+totalCommon sort fields include created_at (chronological), updated_at (recent activity), name or title (alphabetical), and computed fields like relevance for search results.
Pagination
Pagination controls how much data is returned at once. Without it, your API can become slow, crash under load, or send huge responses. . You have two fundamentally different approaches, each with distinct tradeoffs.
Offset-Based Pagination
This is the most simple approach out there. Skip certain number of records then grab certain number of records . Simple to implement, easy for clients to understand, and sufficient for small, stable datasets.
GET /orders?page=2&limit=20Response:
{
"data": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 100
}
}The implementation is straightforward. The database executes LIMIT 20 OFFSET 20, the API wraps results with metadata, and clients navigate by incrementing page numbers.
But offset pagination harbors subtle, serious flaws. First, performance degrades linearly with depth. OFFSET 100000 forces the database to scan and discard 100,000 rows before returning 20. At scale, page 5000 is orders of magnitude slower than page 1. Second, and more insidiously, results shift during navigation. If a new order is created while a client paginates, the entire result set shifts. Record 21 becomes record 22. The client sees duplicates or misses records entirely. For high-velocity data or financial records, this inconsistency is unacceptable.
Cursor-Based Pagination
In this approach, a client receives a cursor indicating "here is where you are," and requests the next page relative to that position.
GET /orders?cursor=eyJpZCI6NTAwfQ==&limit=20Response:
{
"data": [...],
"meta": {
"total": 1543,
"page": 2,
"limit": 20,
"next_cursor": "eyJpZCI6NTAwfQ==",
"has_more": true
}
}The cursor typically encodes the last seen record's sort fields, often base64-encoded JSON like {"id": 500, "created_at": "2024-01-15T10:30:00Z"}. The database queries WHERE (created_at, id) > (last_seen_time, last_seen_id), leveraging indexed seeks. This is fast at any scale, page 1 and page 5000 execute in identical time. More importantly, it is consistent. New insertions do not shift the relative position of existing records. The client sees a stable snapshot of data as it existed when they started paginating.
The tradeoff is flexibility. You cannot jump to arbitrary pages. There is no "page 5 of 10." Navigation is strictly forward (and sometimes backward with reverse cursors), making cursor pagination ideal for infinite scroll feeds, real-time data, and high-velocity collections. Offset pagination remains suitable for administrative interfaces, small datasets, and situations where users demand direct page access.
Error Handling
Error handling is a crucial part of API design because things will go wrong, invalid input, missing data, or server issues. A well-designed API doesn’t just fail; it fails clearly and consistently, so developers can quickly understand and fix the problem.
Instead of returning random messages, always use a structured error format.
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found"
}
}- Use a consistent error structure across all endpoints
- Include both:
- Error code (for systems)
- Message (for humans)
- . Return proper HTTP status codes
- 400 → bad request
- 404 → not found
- 401 → unauthorized
- 500 → server error
- Be specific about what went wrong
Rate Limiting
Rate limiting protects your API from abuse, accidental loops, and resource exhaustion. It ensures fair usage across clients and maintains service stability under load.
Headers Communicate limits to clients:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1705312800
Retry-After: 3600Return 429 Too Many Requests when exceeded. Include Retry-After header indicating seconds to wait.
Rate limiting is infrastructure, not business logic. Implement at the edge (API gateway, reverse proxy) when possible, application layer when necessary.
Caching
Caching reduces latency, decreases server load, and improves availability. REST's statelessness and uniform interface make HTTP caching particularly effective.
Cache Locations
| Layer | Technology | Scope |
|---|---|---|
| Browser | HTTP cache headers | Single user, private data |
| CDN | Cloudflare, Fastly, AWS CloudFront | Global, public data |
| Reverse Proxy | Nginx, Varnish | Regional, mixed data |
| Application | Redis, Memcached | Computed data, sessions |
| Database | Query cache, materialized views | Query results |
HTTP Cache Headers
| Header | Purpose | Example |
|---|---|---|
Cache-Control | Primary directive | public, max-age=3600 |
Expires | Absolute expiration date | Wed, 15 Jan 2024 10:30:00 GMT |
ETag | Resource version identifier | "abc123" |
Last-Modified | Modification timestamp | Wed, 15 Jan 2024 10:30:00 GMT |
Vary | Cache key variations | Accept-Encoding, Authorization |
Cache-Control Directives
public → Cacheable by any cache
private → Cacheable by browser only
no-cache → Revalidate with server before using
no-store → Never cache (sensitive data)
max-age=3600 → Cache for 3600 seconds
s-maxage=3600 → Shared cache max-age (CDNs)
must-revalidate → Obey freshness information strictly
immutable → Content never changes (versioned assets)Cache aggressively what changes infrequently. Cache cautiously what changes often or contains user-specific data. Always validate before relying on cached data for critical operations.
Conclusion
We've covered the foundations of REST API design: resources as the central abstraction, HTTP methods and status codes as the communication protocol, and the practical mechanisms that make APIs usable at scale, pagination, filtering, sorting, versioning, error handling, caching, rate limiting, and performance optimization.
The core principle throughout: REST is an architectural style, not a protocol. Treat it as such and you inherit the web's scalability, reliability, and evolvability. Treat it as "HTTP with JSON" and you miss the constraints that make distributed systems work.
Key takeaways:
- Design resources, not endpoints. Nouns in URIs, verbs in HTTP methods.
- Leverage HTTP semantics. Status codes communicate outcomes, headers enable caching and negotiation.
- Plan for evolution. Version explicitly, deprecate gracefully, change additively.
- Handle errors consistently. Clear codes, structured responses, no stack traces in production.
- Optimize deliberately. Measure first, cache appropriately, paginate always.
These fundamentals separate APIs that merely function from APIs that scale, survive, and improve over time. The rest, authentication patterns, hypermedia, advanced security, is built upon this foundation.
Build deliberately. Design for the client you have and the clients you haven't met yet.
Related Articles
Understanding Golang Packages And Modules
Go’s simplicity hides powerful concepts like packages and modules that make large-scale applications maintainable and efficient. In this guide, we break down how packages structure your code and how modules handle dependencies in modern Go development.

Understanding How the Web Actually Works (HTTP Explained Simply)
I used APIs every day without truly understanding what was happening under the hood. In this post, I break down HTTP, requests, responses, and how the web actually works, in a way that finally made things click for me.

The Complete API Architecture Guide: REST, GraphQL, gRPC, tRPC, WebSockets & SSE
Navigate the complex landscape of API architectures with data-driven insights. From REST's reliability to gRPC's 10x performance gains, understand which protocol fits your use case, team structure, and scalability requirements.