REST API Fundamentals: HTTP Methods, Status Codes & HATEOAS
This article is a comprehensive explanation of REST API fundamentals – including HTTP methods, status codes, and HATEOAS principles.
In a Nutshell
REST is an architectural style for distributed systems that uses HTTP methods, status codes, and HATEOAS to create scalable and stateless web services.
Compact Technical Description
Representational State Transfer (REST) is an architectural style for web services defined by Roy Fielding. REST uses the semantics of HTTP for operations on resources.
Core Principles:
- Client-Server: Separation of responsibilities
- Stateless: No session states on server side
- Cacheable: Responses can be cached
- Uniform Interface: Uniform interface via HTTP
- Layered System: Intermediate layers possible
- Code on Demand: Optional: Server can send code to client
HTTP Methods:
- GET: Read resource (safe, idempotent)
- POST: Create resource (not safe, not idempotent)
- PUT: Replace resource (not safe, idempotent)
- PATCH: Partially modify resource (not safe, not idempotent)
- DELETE: Delete resource (not safe, idempotent)
HATEOAS (Hypermedia as the Engine of Application State) enables navigation through APIs without hard-coded URLs.
Exam-Relevant Key Points
- HTTP Methods: GET, POST, PUT, DELETE with correct semantics
- Status Codes: 2xx (Success), 3xx (Redirect), 4xx (Client Error), 5xx (Server Error)
- HATEOAS: Hypermedia as application control
- Stateless: No session states on server side
- Richardson Maturity Model: Maturity level model for REST APIs
- Resource Naming: Consistent URI structure and nomenclature
- IHK-relevant for web development and software architecture
Core Components
- Resources: Unique identifiers (URIs) for data
- HTTP Methods: CRUD operations via HTTP verbs
- Status Codes: Standardized response codes
- Representations: JSON, XML, HTML as data formats
- Hypermedia: Links for navigation between resources
- Statelessness: Every request contains all required information
- Cacheability: Responses can be cached
- Layered System: Load balancers, proxies, gateways
Practical Examples
1. Resource Design and HTTP Methods
// Express.js REST API example
const express = require('express');
const app = express();
app.use(express.json());
// Data (in-memory)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
// GET /users - Read all users
app.get('/users', (req, res) => {
res.status(200).json({
users: users,
_links: {
self: { href: '/users' },
create: { href: '/users', method: 'POST' }
}
});
});
// GET /users/{id} - Read single user
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({
error: 'User not found',
_links: {
users: { href: '/users' }
}
});
}
res.status(200).json({
user: user,
_links: {
self: { href: `/users/${user.id}` },
update: { href: `/users/${user.id}`, method: 'PUT' },
delete: { href: `/users/${user.id}`, method: 'DELETE' },
users: { href: '/users' }
}
});
});
// POST /users - Create new user
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({
error: 'Name and email are required',
_links: {
users: { href: '/users' }
}
});
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json({
message: 'User created successfully',
user: newUser,
_links: {
self: { href: `/users/${newUser.id}` },
users: { href: '/users' }
}
});
});
// PUT /users/{id} - Completely replace user
app.put('/users/:id', (req, res) => {
const { name, email } = req.body;
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({
error: 'User not found',
_links: {
users: { href: '/users' }
}
});
}
if (!name || !email) {
return res.status(400).json({
error: 'Name and email are required',
_links: {
user: { href: `/users/${userId}` }
}
});
}
users[userIndex] = { id: userId, name, email };
res.status(200).json({
message: 'User updated successfully',
user: users[userIndex],
_links: {
self: { href: `/users/${userId}` },
users: { href: '/users' }
}
});
});
// DELETE /users/{id} - Delete user
app.delete('/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex === -1) {
return res.status(404).json({
error: 'User not found',
_links: {
users: { href: '/users' }
}
});
}
users.splice(userIndex, 1);
res.status(200).json({
message: 'User deleted successfully',
_links: {
users: { href: '/users' },
create: { href: '/users', method: 'POST' }
}
});
});
app.listen(3000, () => {
console.log('REST API Server running on port 3000');
});
2. Spring Boot REST API with HATEOAS
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// GET /api/products - All products with HATEOAS
@GetMapping
public ResponseEntity<CollectionModel<EntityModel<Product>>> getAllProducts() {
List<Product> products = productService.findAll();
List<EntityModel<Product>> productModels = products.stream()
.map(product -> EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(product.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products")
))
.collect(Collectors.toList());
CollectionModel<EntityModel<Product>> collectionModel =
CollectionModel.of(productModels,
linkTo(methodOn(ProductController.class).getAllProducts()).withSelfRel()
);
return ResponseEntity.ok(collectionModel);
}
// GET /api/products/{id} - Single product with HATEOAS
@GetMapping("/{id}")
public ResponseEntity<EntityModel<Product>> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(product -> EntityModel.of(product,
linkTo(methodOn(ProductController.class).getProduct(id)).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products"),
linkTo(methodOn(ProductController.class).updateProduct(id, null)).withRel("update"),
linkTo(methodOn(ProductController.class).deleteProduct(id)).withRel("delete")
))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/products - Create product
@PostMapping
public ResponseEntity<EntityModel<Product>> createProduct(@RequestBody Product product) {
Product createdProduct = productService.save(product);
EntityModel<Product> productModel = EntityModel.of(createdProduct,
linkTo(methodOn(ProductController.class).getProduct(createdProduct.getId())).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products")
);
return ResponseEntity
.created(URI.create("/api/products/" + createdProduct.getId()))
.body(productModel);
}
// PUT /api/products/{id} - Update product
@PutMapping("/{id}")
public ResponseEntity<EntityModel<Product>> updateProduct(
@PathVariable Long id, @RequestBody Product product) {
return productService.findById(id)
.map(existingProduct -> {
product.setId(id);
Product updatedProduct = productService.save(product);
EntityModel<Product> productModel = EntityModel.of(updatedProduct,
linkTo(methodOn(ProductController.class).getProduct(id)).withSelfRel(),
linkTo(methodOn(ProductController.class).getAllProducts()).withRel("products")
);
return ResponseEntity.ok(productModel);
})
.orElse(ResponseEntity.notFound().build());
}
// DELETE /api/products/{id} - Delete product
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
if (productService.existsById(id)) {
productService.deleteById(id);
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}
3. Python Flask REST API
from flask import Flask, jsonify, request, url_for
from werkzeug.exceptions import NotFound, BadRequest
app = Flask(__name__)
# In-memory database
products = [
{'id': 1, 'name': 'Laptop', 'price': 999.99, 'category': 'Electronics'},
{'id': 2, 'name': 'Mouse', 'price': 29.99, 'category': 'Electronics'}
]
def generate_links(product_id=None):
"""Generate HATEOAS links"""
links = {
'products': {'href': url_for('get_products', _external=True)}
}
if product_id:
links.update({
'self': {'href': url_for('get_product', id=product_id, _external=True)},
'update': {'href': url_for('update_product', id=product_id, _external=True)},
'delete': {'href': url_for('delete_product', id=product_id, _external=True)}
})
return links
@app.route('/api/products', methods=['GET'])
def get_products():
"""Retrieve all products"""
return jsonify({
'products': products,
'_links': generate_links()
}), 200
@app.route('/api/products/<int:product_id>', methods=['GET'])
def get_product(product_id):
"""Retrieve single product"""
product = next((p for p in products if p['id'] == product_id), None)
if not product:
return jsonify({
'error': 'Product not found',
'_links': generate_links()
}), 404
return jsonify({
'product': product,
'_links': generate_links(product_id)
}), 200
@app.route('/api/products', methods=['POST'])
def create_product():
"""Create new product"""
data = request.get_json()
if not data or 'name' not in data or 'price' not in data:
return jsonify({
'error': 'Name and price are required',
'_links': generate_links()
}), 400
new_product = {
'id': len(products) + 1,
'name': data['name'],
'price': data['price'],
'category': data.get('category', 'Uncategorized')
}
products.append(new_product)
return jsonify({
'message': 'Product created successfully',
'product': new_product,
'_links': generate_links(new_product['id'])
}), 201
@app.route('/api/products/<int:product_id>', methods=['PUT'])
def update_product(product_id):
"""Update product"""
product = next((p for p in products if p['id'] == product_id), None)
if not product:
return jsonify({
'error': 'Product not found',
'_links': generate_links()
}), 404
data = request.get_json()
if not data or 'name' not in data or 'price' not in data:
return jsonify({
'error': 'Name and price are required',
'_links': generate_links(product_id)
}), 400
product.update({
'name': data['name'],
'price': data['price'],
'category': data.get('category', product['category'])
})
return jsonify({
'message': 'Product updated successfully',
'product': product,
'_links': generate_links(product_id)
}), 200
@app.route('/api/products/<int:product_id>', methods=['DELETE'])
def delete_product(product_id):
"""Delete product"""
global products
product = next((p for p in products if p['id'] == product_id), None)
if not product:
return jsonify({
'error': 'Product not found',
'_links': generate_links()
}), 404
products = [p for p in products if p['id'] != product_id]
return jsonify({
'message': 'Product deleted successfully',
'_links': generate_links()
}), 200
if __name__ == '__main__':
app.run(debug=True)
HTTP Status Codes
2xx Success
- 200 OK: Request successful
- 201 Created: Resource created
- 204 No Content: Request successful, no return
3xx Redirection
- 301 Moved Permanently: Permanent redirect
- 302 Found: Temporary redirect
- 304 Not Modified: Content unchanged (Cache)
4xx Client Errors
- 400 Bad Request: Invalid request
- 401 Unauthorized: Authentication required
- 403 Forbidden: Access denied
- 404 Not Found: Resource not found
- 409 Conflict: Conflict with existing state
5xx Server Errors
- 500 Internal Server Error: Server error
- 502 Bad Gateway: Gateway/proxy error
- 503 Service Unavailable: Service not available
Richardson Maturity Model
Level 0: Swamp of POX
POST /api/products
{"action": "getAll"}
Level 1: Resources
GET /api/getAllProducts
POST /api/createProduct
Level 2: HTTP Verbs
GET /api/products
POST /api/products
PUT /api/products/123
DELETE /api/products/123
Level 3: Hypermedia (HATEOAS)
{
"product": {
"id": 123,
"name": "Laptop",
"price": 999.99
},
"_links": {
"self": { "href": "/api/products/123" },
"update": { "href": "/api/products/123", "method": "PUT" },
"delete": { "href": "/api/products/123", "method": "DELETE" },
"products": { "href": "/api/products" }
}
}
Advantages and Disadvantages
Advantages of REST
- Scalability: Stateless architecture enables horizontal scaling
- Flexibility: Various data formats (JSON, XML, HTML)
- Simplicity: Uses established HTTP protocol
- Cacheability: Responses can be cached
- Separation: Clear separation of client and server
Disadvantages
- Overhead: HTTP headers and JSON structure
- Statelessness: Requires client-side state management
- Versioning: API versioning can be complex
- Security: HTTPS and authentication required
Common Exam Questions
-
What is the difference between PUT and PATCH? PUT replaces the entire resource, PATCH only modifies parts of the resource.
-
Explain HATEOAS! Hypermedia as the engine of application state - clients navigate through links without fixed URLs.
-
Why is statelessness important for REST? Enables horizontal scaling and simplifies load distribution.
-
What does idempotent mean for HTTP methods? Multiple executions lead to the same result (GET, PUT, DELETE).
Most Important Sources
- https://restfulapi.net/
- https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- https://www.jsonapi.org/