REST API Grundlagen: HTTP-Methoden, Statuscodes & HATEOAS
Dieser Beitrag ist eine umfassende Erläuterung der REST-API Grundlagen – inklusive HTTP-Methoden, Statuscodes und HATEOAS Prinzipien.
In a Nutshell
REST ist ein architektonischer Stil für verteilte Systeme, der HTTP-Methoden, Statuscodes und HATEOAS nutzt, um skalierbare und zustandslose Web-Services zu erstellen.
Kompakte Fachbeschreibung
Representational State Transfer (REST) ist ein von Roy Fielding definierter architektonischer Stil für Web-Services. REST nutzt die Semantik von HTTP für Operationen auf Ressourcen.
Kernprinzipien:
- Client-Server: Trennung von Verantwortlichkeiten
- Stateless: Keine Sitzungszustände auf Server-Seite
- Cacheable: Antworten können zwischengespeichert werden
- Uniform Interface: Einheitliche Schnittstelle über HTTP
- Layered System: Zwischenschichten möglich
- Code on Demand: Optional: Server kann Code an Client senden
HTTP-Methoden:
- GET: Ressource lesen (sicher, idempotent)
- POST: Ressource erstellen (nicht sicher, nicht idempotent)
- PUT: Ressource ersetzen (nicht sicher, idempotent)
- PATCH: Ressource teilweise ändern (nicht sicher, nicht idempotent)
- DELETE: Ressource löschen (nicht sicher, idempotent)
HATEOAS (Hypermedia as the Engine of Application State) ermöglicht Navigation durch APIs ohne fest codierte URLs.
Prüfungsrelevante Stichpunkte
- HTTP-Methoden: GET, POST, PUT, DELETE mit korrekter Semantik
- Statuscodes: 2xx (Erfolg), 3xx (Umleitung), 4xx (Client-Fehler), 5xx (Server-Fehler)
- HATEOAS: Hypermedia als Anwendungssteuerung
- Stateless: Keine Sitzungszustände auf Server-Seite
- Richardson Maturity Model: Reifegradmodell für REST-APIs
- Resource Naming: Konsistente URI-Struktur und Nomenklatur
- IHK-relevant für Webentwicklung und Softwarearchitektur
Kernkomponenten
- Resources: Eindeutige Identifikatoren (URIs) für Daten
- HTTP Methods: CRUD-Operationen über HTTP-Verben
- Status Codes: Standardisierte Antwort-Codes
- Representations: JSON, XML, HTML als Datenformate
- Hypermedia: Links für Navigation zwischen Ressourcen
- Statelessness: Jede Anfrage enthält alle benötigten Informationen
- Cacheability: Antworten können zwischengespeichert werden
- Layered System: Load Balancer, Proxies, Gateways
Praxisbeispiele
1. Resource Design und HTTP-Methoden
// Express.js REST API Beispiel
const express = require('express');
const app = express();
app.use(express.json());
// Daten (in-memory)
let users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
// GET /users - Alle Benutzer lesen
app.get('/users', (req, res) => {
res.status(200).json({
users: users,
_links: {
self: { href: '/users' },
create: { href: '/users', method: 'POST' }
}
});
});
// GET /users/{id} - Einzelnen Benutzer lesen
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 - Neuen Benutzer erstellen
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} - Benutzer vollständig ersetzen
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} - Benutzer löschen
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 mit HATEOAS
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// GET /api/products - Alle Produkte mit 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} - Einzelnes Produkt mit 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 - Produkt erstellen
@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} - Produkt aktualisieren
@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} - Produkt löschen
@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 Datenbank
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):
"""HATEOAS Links generieren"""
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():
"""Alle Produkte abrufen"""
return jsonify({
'products': products,
'_links': generate_links()
}), 200
@app.route('/api/products/<int:product_id>', methods=['GET'])
def get_product(product_id):
"""Einzelnes Produkt abrufen"""
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():
"""Neues Produkt erstellen"""
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):
"""Produkt aktualisieren"""
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):
"""Produkt löschen"""
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-Statuscodes
2xx Success
- 200 OK: Anfrage erfolgreich
- 201 Created: Ressource erstellt
- 204 No Content: Anfrage erfolgreich, keine Rückgabe
3xx Redirection
- 301 Moved Permanently: Permanente Weiterleitung
- 302 Found: Temporäre Weiterleitung
- 304 Not Modified: Inhalt unverändert (Cache)
4xx Client Errors
- 400 Bad Request: Ungültige Anfrage
- 401 Unauthorized: Authentifizierung erforderlich
- 403 Forbidden: Zugriff verweigert
- 404 Not Found: Ressource nicht gefunden
- 409 Conflict: Konflikt mit vorhandenem Zustand
5xx Server Errors
- 500 Internal Server Error: Serverfehler
- 502 Bad Gateway: Gateway/Proxy-Fehler
- 503 Service Unavailable: Dienst nicht verfügbar
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" }
}
}
Vorteile und Nachteile
Vorteile von REST
- Skalierbarkeit: Stateless Architektur ermöglicht horizontale Skalierung
- Flexibilität: Verschiedene Datenformate (JSON, XML, HTML)
- Einfachheit: Nutzt etabliertes HTTP-Protokoll
- Cacheability: Antworten können zwischengespeichert werden
- Trennung: Klare Trennung von Client und Server
Nachteile
- Overhead: HTTP-Header und JSON-Struktur
- Statelessness: Erfordert Client-seitige Zustandsverwaltung
- Versionierung: API-Versionierung kann komplex werden
- Sicherheit: HTTPS und Authentifizierung erforderlich
Häufige Prüfungsfragen
-
Was ist der Unterschied zwischen PUT und PATCH? PUT ersetzt die gesamte Ressource, PATCH ändert nur Teile der Ressource.
-
Erklären Sie HATEOAS! Hypermedia als Anwendungssteuerung - Clients navigieren durch Links ohne feste URLs.
-
Warum ist Stateless wichtig für REST? Ermöglicht horizontale Skalierung und vereinfacht Lastverteilung.
-
Was bedeutet idempotent bei HTTP-Methoden? Mehrmalige Ausführung führt zum gleichen Ergebnis (GET, PUT, DELETE).
Wichtigste Quellen
- https://restfulapi.net/
- https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- https://www.jsonapi.org/