Skip to content
IRC-Coding IRC-Coding
REST API HTTP Methoden Statuscodes HATEOAS Richardson Maturity

REST API Grundlagen: HTTP-Methoden, Statuscodes & HATEOAS

REST-APIs mit HTTP-Methoden, Statuscodes, HATEOAS und Richardson Maturity Model. Praktische Beispiele für GET, POST, PUT, DELETE mit Best Practices.

S

schutzgeist

2 min read

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

  1. Resources: Eindeutige Identifikatoren (URIs) für Daten
  2. HTTP Methods: CRUD-Operationen über HTTP-Verben
  3. Status Codes: Standardisierte Antwort-Codes
  4. Representations: JSON, XML, HTML als Datenformate
  5. Hypermedia: Links für Navigation zwischen Ressourcen
  6. Statelessness: Jede Anfrage enthält alle benötigten Informationen
  7. Cacheability: Antworten können zwischengespeichert werden
  8. 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

  1. Was ist der Unterschied zwischen PUT und PATCH? PUT ersetzt die gesamte Ressource, PATCH ändert nur Teile der Ressource.

  2. Erklären Sie HATEOAS! Hypermedia als Anwendungssteuerung - Clients navigieren durch Links ohne feste URLs.

  3. Warum ist Stateless wichtig für REST? Ermöglicht horizontale Skalierung und vereinfacht Lastverteilung.

  4. Was bedeutet idempotent bei HTTP-Methoden? Mehrmalige Ausführung führt zum gleichen Ergebnis (GET, PUT, DELETE).

Wichtigste Quellen

  1. https://restfulapi.net/
  2. https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
  3. https://www.jsonapi.org/
Zurück zum Blog
Share:

Ähnliche Beiträge