What is REST and REST principles — REST API
REST (Representational State Transfer) is an architectural style for networked applications where clients interact with server-managed resources using standard HTTP methods and URIs.
Theory
TL;DR
- REST works like a public library: clients request resources by ID using standard methods (GET, POST, PUT, DELETE), and the server manages storage independently
- The core constraint is statelessness: every request carries all needed context (auth token, pagination cursor), and the server keeps no memory between calls
- REST fits public APIs, CRUD services, and browser-accessible resources. Skip it for real-time bidirectional communication (use WebSockets) or high-speed internal services with binary payloads (use gRPC)
- REST has six constraints: client-server, stateless, cacheable, uniform interface, layered system, code-on-demand (optional)
- "RESTful" means following all six. Most APIs that call themselves REST skip HATEOAS and are really just HTTP APIs
Quick Example
Basic Express.js REST API for a /users resource:
const express = require('express');
const app = express();
app.use(express.json());
let users = [{ id: 1, name: 'Alice' }];
app.get('/users', (req, res) => res.json(users)); // Read all
app.post('/users', (req, res) => {
const user = { id: users.length + 1, name: req.body.name };
users.push(user);
res.status(201).json(user); // 201 Created
});
app.put('/users/:id', (req, res) => {
const user = users.find(u => u.id == req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
user.name = req.body.name;
res.json(user);
});
app.delete('/users/:id', (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).send(); // 204 No Content
});
app.listen(3000);Each HTTP verb maps to exactly one CRUD operation. The server holds no session between calls.
The Six REST Constraints
Roy Fielding defined REST in his 2000 dissertation. Six constraints together produce predictable, scalable APIs.
1. Client-server. The UI and data layer are separate. The client does not care where data lives; the server does not care how the UI renders it.
2. Stateless. Every request contains everything the server needs: auth token, pagination cursor, content type. No session stored on the server. This is the constraint that matters most in practice.
3. Cacheable. Responses declare whether they can be cached via Cache-Control or ETag headers. Correct caching cuts server load significantly.
4. Uniform interface. Four sub-constraints: resources identified by URI, manipulated via representations (JSON or XML), self-descriptive messages (HTTP headers carry content type and auth), and HATEOAS (responses include links to related actions).
5. Layered system. The client talks to one endpoint but may pass through load balancers, CDN nodes, or API gateways. Each layer sees only the adjacent one.
6. Code-on-demand (optional). The server can send executable code to the client. Most REST APIs do not use this.
Constraints 1 through 5 are the practical baseline. HATEOAS is the part most teams skip.
Stateless vs Stateful: the Real Difference
Statelessness is what makes REST scale horizontally. Any server in a cluster can handle any request because there is no shared session to worry about. The tradeoff is straightforward: requests become larger. A JWT token can be 500+ bytes. A server-side session pointer is 50 bytes. For public APIs at the scale of GitHub or Stripe, that tradeoff is worth it. For a private internal tool with two servers, it is less obvious.
When to Use REST
- Public API with many client types (browsers, mobile apps, third-party integrations): REST
- Standard CRUD operations on well-defined resources: REST
- Microservices where teams use different languages: REST (HTTP is universal)
- Real-time bidirectional communication (chat, live notifications): WebSockets
- High-performance internal services with binary payloads: gRPC
- Complex queries where clients need to specify exactly what fields to fetch: GraphQL
Protocol Comparison
| Aspect | REST | SOAP | GraphQL |
|---|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP, SMTP, TCP | HTTP |
| Data format | JSON, XML | XML only | JSON |
| State | Stateless | Stateful possible | Stateless |
| Query flexibility | Fixed endpoints | XML envelope operations | Client-defined |
| Request overhead | Low | High | Low to medium |
| Error handling | HTTP status codes | SOAP faults | Custom errors in body |
| Best for | Web APIs, browsers (Stripe) | Enterprise WS-Security (banking) | Complex queries (GitHub v4) |
Common Mistakes
In production, the most common REST mistake is not wrong HTTP verbs. It is teams breaking their own statelessness without noticing.
Storing state on the server. The classic: sessions[userId].cart = items. Works on one server. Breaks when the load balancer sends the next request to a different machine. Fix: store cart state in a JWT and pass it in every request.
// Wrong: breaks with multiple servers
app.post('/cart/add', (req, res) => {
sessions[req.sessionId].items.push(req.body);
});
// Fix: stateless approach with JWT
app.post('/cart/add', (req, res) => {
const cart = jwt.verify(req.headers.authorization, secret).cart;
cart.items.push(req.body);
res.json({ token: jwt.sign({ cart }, secret) });
});Using POST for reads. POST is not idempotent. If a client retries a failed POST, it may create duplicate records. Use GET for reads: it is safe and idempotent by the HTTP specification.
Returning 200 for everything. Sending { error: 'Not found' } with status 200 means monitoring tools, client error handlers, and API clients all miss the failure. Return 404, 422, 500 where they apply.
No versioning from the start. Twitter's migration from API v1 to v2 broke thousands of integrations. Add /api/v1/ from day one.
Treating HTTP verbs as labels. PUT must be idempotent: calling it twice should produce the same result. PATCH updates partial state. DELETE should be safe to call twice. Breaking these contracts breaks client retry logic.
Real-world Usage
- Stripe:
GET /v1/customers/:idreturns customer data with HATEOAS links to subscriptions and charges - GitHub API:
PUT /repos/:owner/:repofor updates,GET /repos?page=2for cursor pagination - Twitter API v2:
POST /2/tweetsfor creating tweets with media arrays - Express.js:
app.use('/api/v1', router)as the standard versioned route structure - React + Axios:
fetch('/api/users', { headers: { Authorization: 'Bearer token' } })for stateless auth
Follow-up Questions
Q: What are the six REST constraints?
A: Client-server, stateless, cacheable, uniform interface, layered system, code-on-demand (optional). Constraints one through five are required for a truly RESTful API.
Q: What is HATEOAS and why do most APIs skip it?
A: Hypermedia As The Engine Of Application State means responses include links to possible next actions. A GET /users/1 response would contain links to edit and delete that user. Most teams skip it because it adds payload size and complexity, and clients tend to hardcode URLs anyway.
Q: What is the difference between REST and RESTful?
A: REST is the architectural style Fielding described. RESTful means an implementation follows all six constraints. Most APIs people call REST are really HTTP APIs that skip HATEOAS.
Q: How do you handle authentication statelessly?
A: With JWT Bearer tokens. The token contains user identity and is signed on the server. Every request includes it in the Authorization header. No session table needed. For SPAs, the OAuth2 PKCE flow handles the token exchange securely.
Q: Design a REST API for a photo-sharing app with uploads over 2GB, cursor pagination, and real-time likes.
A: POST /photos with multipart upload and resumable uploads via the Tus.io protocol. GET /photos?cursor=abc&limit=20 for keyset pagination, which stays stable as new photos arrive. WebSockets or SSE for real-time likes since REST polling is too slow here. ETags plus a CDN layer for photo metadata caching.
Examples
Basic: CRUD with correct HTTP status codes
const express = require('express');
const app = express();
app.use(express.json());
let users = [{ id: 1, name: 'Alice' }];
app.get('/users', (req, res) => {
res.json(users); // 200 OK by default
});
app.post('/users', (req, res) => {
const user = { id: users.length + 1, name: req.body.name };
users.push(user);
res.status(201).json(user); // 201 Created, not 200
});
app.put('/users/:id', (req, res) => {
const user = users.find(u => u.id == req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' }); // 404, not 200
user.name = req.body.name;
res.json(user);
});
app.delete('/users/:id', (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).send(); // 204 No Content
});
app.listen(3000, () => console.log('Server running on port 3000'));Status codes are not decoration. 201 tells the client a resource was created. 204 signals success with no body. 404 routes to an error handler. Using 200 for all of these breaks standard tooling.
Intermediate: Stateless pagination with Bearer token auth
The client sends auth and pagination state on every request. Nothing lives on the server between calls.
Server:
app.get('/api/v1/users', (req, res) => {
const auth = req.headers.authorization?.split(' ')[1];
if (!auth) return res.status(401).json({ error: 'Unauthorized' });
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const start = (page - 1) * limit;
const paginatedUsers = users.slice(start, start + limit);
res.json({
data: paginatedUsers,
pagination: { page, limit, total: users.length }
// Output: { data: [{ id: 1, name: 'Alice' }], pagination: { page: 1, limit: 10, total: 1 } }
});
});Client (React):
useEffect(() => {
fetch('/api/v1/users?page=1&limit=10', {
headers: { Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9...' }
})
.then(res => res.json())
.then(data => setUsers(data.data));
}, []);The token carries user identity. The query string carries pagination state. The server had no memory of this client before this request arrived.
Advanced: HATEOAS with ETag caching
This is closer to what Fielding actually described. Responses include links to next actions; ETags let clients skip downloads when data has not changed.
app.get('/api/v1/users/:id', (req, res) => {
const user = users.find(u => u.id == req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
const etag = `"${user.id}-v${user.version || 1}"`;
res.set('ETag', etag);
res.set('Cache-Control', 'private, max-age=60');
// 304 Not Modified if client sends a matching ETag
if (req.headers['if-none-match'] === etag) {
return res.status(304).send();
}
res.json({
id: user.id,
name: user.name,
links: [
{ rel: 'self', href: `/api/v1/users/${user.id}`, method: 'GET' },
{ rel: 'edit', href: `/api/v1/users/${user.id}`, method: 'PUT' },
{ rel: 'delete', href: `/api/v1/users/${user.id}`, method: 'DELETE' }
]
});
});On the second request:
fetch('/api/v1/users/1', {
headers: { 'If-None-Match': '"1-v1"' }
});
// Returns 304 if nothing changed - no body, less bandwidthHATEOAS means the client discovers available actions from the response itself. No need to guess what endpoints exist. Most teams adopt this selectively for resource-heavy endpoints where caching or bandwidth matters.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.