Skip to content
IRC-Coding IRC-Coding
REST API HTTP Methods Status Codes HATEOAS Richardson Maturity

REST API Fundamentals: HTTP Methods & Status Codes

Master REST APIs with HTTP methods, status codes, HATEOAS, and Richardson Maturity Model. Practical examples and best practices.

S

schutzgeist

2 min read
REST API Fundamentals: HTTP Methods & Status Codes

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

  1. Resources: Unique identifiers (URIs) for data
  2. HTTP Methods: CRUD operations via HTTP verbs
  3. Status Codes: Standardized response codes
  4. Representations: JSON, XML, HTML as data formats
  5. Hypermedia: Links for navigation between resources
  6. Statelessness: Every request contains all required information
  7. Cacheability: Responses can be cached
  8. 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

  1. What is the difference between PUT and PATCH? PUT replaces the entire resource, PATCH only modifies parts of the resource.

  2. Explain HATEOAS! Hypermedia as the engine of application state - clients navigate through links without fixed URLs.

  3. Why is statelessness important for REST? Enables horizontal scaling and simplifies load distribution.

  4. What does idempotent mean for HTTP methods? Multiple executions lead to the same result (GET, PUT, DELETE).

Most Important Sources

  1. https://restfulapi.net/
  2. https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
  3. https://www.jsonapi.org/
Back to Blog
Share:

Related Posts