REST API Fundamentals: HTTP Methods, Status Codes & HATEOAS
This guide covers the core principles 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 build scalable, stateless web services.
Core Definition
Representational State Transfer (REST) is an architectural style for web services defined by Roy Fielding. REST leverages HTTP semantics to perform operations on resources.
Key principles:
- Client-Server: Clear separation of concerns
- Stateless: No session state stored on the server
- Cacheable: Responses can be cached
- Uniform Interface: Consistent interface over HTTP
- Layered System: Intermediary layers are possible
- Code on Demand: Optional — server can send code to the client
HTTP Methods:
- GET: Read a resource (safe, idempotent)
- POST: Create a resource (unsafe, non-idempotent)
- PUT: Replace a resource entirely (unsafe, idempotent)
- PATCH: Partially update a resource (unsafe, non-idempotent)
- DELETE: Delete a resource (unsafe, idempotent)
HATEOAS (Hypermedia as the Engine of Application State) enables navigation through APIs without hardcoded URLs.
Key Concepts to Remember
- HTTP Methods: GET, POST, PUT, DELETE with correct semantics
- Status Codes: 2xx (success), 3xx (redirection), 4xx (client error), 5xx (server error)
- HATEOAS: Hypermedia for application control
- Stateless: No session state retained on the server
- Richardson Maturity Model: Maturity framework for REST APIs
- Resource Naming: Consistent URI structure and conventions
- Relevant for: Web development and software architecture certifications
Core Components
- Resources: Unique identifiers (URIs) for data entities
- 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: Each request contains all necessary 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());
// In-memory data
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 a 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 a 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} - Replace a user entirely
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 a 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 a 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 a 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 a 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 a 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 a 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 a 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 a 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 with no content to 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 or proxy error
- 503 Service Unavailable: Service unavailable
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: Support for multiple data formats (JSON, XML, HTML)
- Simplicity: Leverages the established HTTP protocol
- Cacheability: Responses can be cached
- Separation of concerns: Clear separation between client and server
Disadvantages
- Overhead: HTTP headers and JSON structure add weight
- Statelessness: Requires client-side state management
- Versioning: API versioning can become complicated
- Security: HTTPS and authentication are essential
Common Exam Questions
-
What’s the difference between PUT and PATCH? PUT replaces the entire resource, while PATCH modifies only parts of it.
-
Explain HATEOAS. Hypermedia as the Engine of Application State—clients navigate through links without hardcoding URLs.
-
Why is statelessness important for REST? It enables horizontal scaling and simplifies load distribution.
-
What does idempotent mean for HTTP methods? Multiple executions produce the same result (GET, PUT, DELETE).
Key Resources
- https://restfulapi.net/
- https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- https://www.jsonapi.org/



