What is REST?
REST (Representational State Transfer) is an architectural style for designing networked applications, particularly web services. It was defined by Roy Fielding in his 2000 doctoral dissertation and has since become the dominant approach for building web APIs.
It's important to understand that REST is not a protocol, standard, or technology—it's a set of architectural constraints and principles. When an API follows these principles, we call it "RESTful." The beauty of REST is that it leverages the existing infrastructure of the web (HTTP) rather than creating something new.
Why REST Became Dominant
Before REST became popular, web services were primarily built using SOAP (Simple Object Access Protocol), which was complex, verbose, and required specialized tooling. REST gained popularity because:
- Simplicity: REST uses standard HTTP, which developers already understand. No special libraries or protocols needed.
- Flexibility: REST can return data in any format (JSON, XML, HTML, plain text), whereas SOAP was limited to XML.
- Scalability: The stateless nature of REST makes it easy to scale by adding more servers.
- Cacheability: REST responses can be cached using standard HTTP caching mechanisms, improving performance dramatically.
- Universal: Any client that can make HTTP requests can consume a REST API—browsers, mobile apps, IoT devices, other servers.
The Core Philosophy: Resources and Representations
The fundamental concept in REST is the resource. A resource is any piece of information that can be named and addressed—users, products, orders, blog posts, comments, etc. Each resource:
- Has a unique identifier (URI/URL)
- Can have multiple representations (JSON, XML, HTML)
- Can be manipulated using a fixed set of operations (HTTP methods)
Key Insight: REST treats everything as a resource that can be accessed and manipulated using standard HTTP methods. Think of resources as nouns (users, products, orders) and HTTP methods as verbs (get, create, update, delete).
- Resource: The "thing" being accessed —
/users/123 - Representation: The format of the data — JSON, XML, etc.
- State Transfer: Passing resource state between client and server
// The REST mental model:
// Resources are NOUNS, HTTP methods are VERBS
Resource: /users ← The collection of all users
Resource: /users/123 ← A specific user (user with ID 123)
Resource: /users/123/orders ← Orders belonging to user 123
// Actions on resources use HTTP methods:
GET /users/123 ← "Get me user 123"
POST /users ← "Create a new user"
PUT /users/123 ← "Replace user 123 with this data"
PATCH /users/123 ← "Update these specific fields of user 123"
DELETE /users/123 ← "Delete user 123"
REST vs SOAP
| Aspect | REST | SOAP |
|---|---|---|
| Protocol | Uses HTTP directly | Protocol-agnostic (usually HTTP) |
| Data Format | JSON, XML, HTML, plain text | XML only |
| Complexity | Lightweight, simple | Heavy, complex |
| Caching | Built-in HTTP caching | No native caching |
| State | Stateless | Can be stateful |
| Use Case | Web APIs, mobile apps | Enterprise, banking |
The 6 REST Constraints
1. Client-Server Separation
// Client and server are independent
// Client doesn't care HOW data is stored
// Server doesn't care HOW data is displayed
// Client (Frontend)
fetch('/api/users/123')
.then(response => response.json())
.then(user => displayUserProfile(user));
// Server (Backend) - Could be Java, Python, Node.js...
@GetMapping("/api/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id);
}
2. Statelessness
// BAD: Server stores client state (NOT RESTful)
@GetMapping("/api/next-page")
public List<Item> getNextPage() {
// Server remembers which page client is on - NOT STATELESS
int currentPage = session.getAttribute("currentPage");
return itemService.getPage(currentPage + 1);
}
// GOOD: Client sends all needed information (RESTful)
@GetMapping("/api/items")
public List<Item> getItems(
@RequestParam int page,
@RequestParam int size) {
// Each request contains everything needed
return itemService.getPage(page, size);
}
// Client includes authentication in EVERY request
GET /api/items?page=2&size=10
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
- Scalability: Any server can handle any request
- Reliability: Server failure doesn't lose client state
- Simplicity: No session synchronization needed
3. Cacheability
// Responses must define themselves as cacheable or not
@GetMapping("/api/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return ResponseEntity.ok()
// Cache for 1 hour
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
// ETag for validation
.eTag(String.valueOf(product.getVersion()))
.body(product);
}
// HTTP Response Headers:
// Cache-Control: max-age=3600
// ETag: "v5"
// For data that changes frequently:
@GetMapping("/api/stock-prices")
public ResponseEntity<List<Price>> getPrices() {
return ResponseEntity.ok()
.cacheControl(CacheControl.noCache()) // Don't cache
.body(priceService.getCurrentPrices());
}
4. Uniform Interface
// The most fundamental constraint - consistent API design
// a) Resource Identification via URIs
GET /api/users/123 // User with ID 123
GET /api/users/123/orders // Orders for user 123
GET /api/orders/456 // Order with ID 456
// b) Manipulation through Representations
// Client receives JSON representation, modifies it, sends back
{
"id": 123,
"name": "John Doe",
"email": "john@example.com" // Client changes this
}
// c) Self-Descriptive Messages
// Request tells server everything it needs
PUT /api/users/123
Content-Type: application/json
Authorization: Bearer token123
{"name": "John Updated"}
// Response tells client everything it needs
HTTP/1.1 200 OK
Content-Type: application/json
Last-Modified: Wed, 22 Jan 2026 10:00:00 GMT
{"id": 123, "name": "John Updated"}
5. Layered System
// Client doesn't know (or care) about intermediate layers
Client
↓
[Load Balancer] ← Client doesn't see this
↓
[API Gateway] ← Or this
↓
[Cache Layer] ← Or this
↓
[Application Server]
↓
[Database]
// Benefits:
// - Security: Hide internal architecture
// - Scalability: Add layers as needed
// - Flexibility: Change layers without client changes
6. Code on Demand (Optional)
// Server can send executable code to client
// This is the only OPTIONAL constraint
// Example: Server sends JavaScript to client
GET /api/validation-rules
// Response:
{
"script": "function validate(email) { return email.includes('@'); }"
}
// Client executes the received code
// (Rarely used in practice due to security concerns)
RESTful URL Design
Resource Naming Conventions
// GOOD: Use nouns, plural form, lowercase
GET /api/users // Collection of users
GET /api/users/123 // Single user
GET /api/users/123/orders // User's orders (sub-resource)
POST /api/users // Create new user
// BAD: Verbs in URLs
GET /api/getUsers // ✗ Don't use verbs
POST /api/createUser // ✗ HTTP method is the verb
GET /api/deleteUser/123 // ✗ Use DELETE method instead
// BAD: Inconsistent naming
GET /api/user // ✗ Use plural (users)
GET /api/User/123 // ✗ Use lowercase
GET /api/user_orders // ✗ Use hyphens or nested resources
HTTP Methods (CRUD Operations)
| Method | CRUD | URL Example | Description |
|---|---|---|---|
GET |
Read | /api/users |
Get all users |
GET |
Read | /api/users/123 |
Get user 123 |
POST |
Create | /api/users |
Create new user |
PUT |
Update | /api/users/123 |
Replace user 123 |
PATCH |
Update | /api/users/123 |
Partial update user 123 |
DELETE |
Delete | /api/users/123 |
Delete user 123 |
Complete REST Controller Example
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
// GET /api/users - Get all users
@GetMapping
public ResponseEntity<List<User>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
List<User> users = userService.findAll(page, size);
return ResponseEntity.ok(users);
}
// GET /api/users/123 - Get single user
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/users - Create new user
@PostMapping
public ResponseEntity<User> createUser(
@Valid @RequestBody CreateUserRequest request) {
User created = userService.create(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
// Return 201 Created with Location header
return ResponseEntity.created(location).body(created);
}
// PUT /api/users/123 - Replace entire user
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.update(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// PATCH /api/users/123 - Partial update
@PatchMapping("/{id}")
public ResponseEntity<User> patchUser(
@PathVariable Long id,
@RequestBody Map<String, Object> updates) {
return userService.patch(id, updates)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/users/123 - Delete user
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.delete(id)) {
return ResponseEntity.noContent().build(); // 204 No Content
}
return ResponseEntity.notFound().build(); // 404 Not Found
}
// GET /api/users/123/orders - Sub-resource
@GetMapping("/{id}/orders")
public ResponseEntity<List<Order>> getUserOrders(
@PathVariable Long id) {
return ResponseEntity.ok(orderService.findByUserId(id));
}
}
HTTP Status Codes
| Code | Name | When to Use |
|---|---|---|
200 |
OK | Successful GET, PUT, PATCH |
201 |
Created | Successful POST (resource created) |
204 |
No Content | Successful DELETE (no body) |
400 |
Bad Request | Invalid request data |
401 |
Unauthorized | Authentication required |
403 |
Forbidden | Authenticated but not authorized |
404 |
Not Found | Resource doesn't exist |
409 |
Conflict | Resource state conflict |
422 |
Unprocessable Entity | Validation errors |
500 |
Internal Server Error | Server-side error |
// Proper status code usage
@PostMapping("/api/users")
public ResponseEntity<?> createUser(@RequestBody UserDTO dto) {
// 400 Bad Request - malformed request
if (dto == null) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("Request body is required"));
}
// 409 Conflict - business rule violation
if (userService.existsByEmail(dto.getEmail())) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("Email already registered"));
}
// 422 Unprocessable Entity - validation failed
List<String> errors = validate(dto);
if (!errors.isEmpty()) {
return ResponseEntity.unprocessableEntity()
.body(new ValidationErrorResponse(errors));
}
// 201 Created - success
User created = userService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
HATEOAS (Hypermedia)
HATEOAS (Hypermedia As The Engine Of Application State) - responses include links to related actions.
// Response without HATEOAS
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
// Response WITH HATEOAS - client discovers available actions
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": { "href": "/api/users/123" },
"orders": { "href": "/api/users/123/orders" },
"delete": { "href": "/api/users/123", "method": "DELETE" },
"update": { "href": "/api/users/123", "method": "PUT" }
}
}
Spring HATEOAS Example
@GetMapping("/{id}")
public EntityModel<User> getUser(@PathVariable Long id) {
User user = userService.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
return EntityModel.of(user,
linkTo(methodOn(UserController.class).getUser(id)).withSelfRel(),
linkTo(methodOn(UserController.class).getUserOrders(id)).withRel("orders"),
linkTo(methodOn(UserController.class).getAllUsers(0, 10)).withRel("users")
);
}
API Versioning Strategies
// 1. URL Path Versioning (Most Common)
GET /api/v1/users
GET /api/v2/users
@RequestMapping("/api/v1/users")
public class UserControllerV1 { }
@RequestMapping("/api/v2/users")
public class UserControllerV2 { }
// 2. Query Parameter Versioning
GET /api/users?version=1
GET /api/users?version=2
// 3. Header Versioning
GET /api/users
Accept: application/vnd.myapi.v1+json
@GetMapping(produces = "application/vnd.myapi.v1+json")
public List<UserV1> getUsersV1() { }
@GetMapping(produces = "application/vnd.myapi.v2+json")
public List<UserV2> getUsersV2() { }
// 4. Custom Header Versioning
GET /api/users
X-API-Version: 2
| Strategy | Pros | Cons |
|---|---|---|
| URL Path | Simple, visible, cacheable | URL pollution |
| Query Param | Optional, easy to default | Easy to forget |
| Header | Clean URLs | Hidden, harder to test |
Error Response Design
// Consistent error response format
public class ApiError {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
private List<FieldError> fieldErrors; // For validation errors
}
// Example error response
{
"timestamp": "2026-01-23T10:30:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"path": "/api/users",
"fieldErrors": [
{
"field": "email",
"message": "must be a valid email address"
},
{
"field": "age",
"message": "must be at least 18"
}
]
}
// Global exception handler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleNotFound(
ResourceNotFoundException ex, HttpServletRequest request) {
ApiError error = new ApiError(
LocalDateTime.now(),
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidation(
MethodArgumentNotValidException ex, HttpServletRequest request) {
List<FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors().stream()
.map(e -> new FieldError(e.getField(), e.getDefaultMessage()))
.toList();
ApiError error = new ApiError(
LocalDateTime.now(),
HttpStatus.BAD_REQUEST.value(),
"Validation Failed",
"Request validation failed",
request.getRequestURI(),
fieldErrors
);
return ResponseEntity.badRequest().body(error);
}
}
Best Practices
DO:
- Use nouns for resources:
/users,/orders,/products - Use HTTP methods correctly: GET for read, POST for create, etc.
- Return appropriate status codes: 201 for created, 404 for not found
- Use plural resource names:
/usersnot/user - Support pagination:
?page=1&size=20 - Support filtering:
?status=active&role=admin - Version your API:
/api/v1/users - Use consistent error format: Same structure for all errors
DON'T:
- Don't use verbs in URLs:
/getUsers,/createUser - Don't ignore status codes: Return 200 for everything
- Don't store state on server: Keep it stateless
- Don't expose internal IDs: Use public identifiers when needed
- Don't return stack traces: Log them, return friendly messages
- Don't ignore security: Always use HTTPS, validate input
Summary
- REST: Architectural style based on resources and HTTP
- Resources: Identified by URIs, manipulated via HTTP methods
- Stateless: Each request contains all information needed
- HTTP Methods: GET (read), POST (create), PUT (replace), PATCH (update), DELETE
- Status Codes: Use appropriate codes (200, 201, 400, 404, etc.)
- HATEOAS: Responses include links to related actions
- Versioning: Plan for API evolution from day one