Skip to main content

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:

javascript
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

AspectRESTSOAPGraphQL
ProtocolHTTP/1.1 or HTTP/2HTTP, SMTP, TCPHTTP
Data formatJSON, XMLXML onlyJSON
StateStatelessStateful possibleStateless
Query flexibilityFixed endpointsXML envelope operationsClient-defined
Request overheadLowHighLow to medium
Error handlingHTTP status codesSOAP faultsCustom errors in body
Best forWeb 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.

javascript
// 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/:id returns customer data with HATEOAS links to subscriptions and charges
  • GitHub API: PUT /repos/:owner/:repo for updates, GET /repos?page=2 for cursor pagination
  • Twitter API v2: POST /2/tweets for 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

javascript
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:

javascript
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):

javascript
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.

javascript
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:

javascript
fetch('/api/v1/users/1', { headers: { 'If-None-Match': '"1-v1"' } }); // Returns 304 if nothing changed - no body, less bandwidth

HATEOAS 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?