programmier-anfang()

Wie man eine REST API mit Spring Boot erstellt — 7 Schritte (2026)

Anna Schneider

Anna Schneider

Backend-Entwicklerin · 7. Juni 2026 · 13 Min. Lesezeit

TL;DR

  • Spring Boot 3.4 + Java 21: Die Standardkombination für professionelle REST APIs in deutschen Unternehmen 2026. Virtual Threads, Pattern Matching, native GraalVM-Kompilierung.
  • 7 Schritte: Initializr → JPA-Entities → Repository → CRUD-Controller → Validierung/DTOs → OpenAPI-Dokumentation → Tests + Docker.
  • Praxisbeispiele: Vollständiger Code für eine Produkt-API — vom Entity über den Controller bis zum Dockerfile. Copy-Paste-fähig.
  • Zeitaufwand: 3–4 Stunden für die Grundimplementierung. 1–2 Tage für produktionsreife Qualität mit Tests und Docker.

Spring Boot ist das mit Abstand populärste Framework für REST APIs in der Java-Welt — und das aus gutem Grund. Laut der JetBrains Developer Survey 2025 nutzen 74% aller Java-Entwickler Spring Boot für Backend-Anwendungen. In deutschen Unternehmen liegt der Anteil sogar bei über 80%, getrieben durch SAP, Deutsche Bank, Allianz und den industriellen Mittelstand.

Dieser Leitfaden zeigt Ihnen in 7 konkreten Schritten, wie Sie eine produktionsreife REST API mit Spring Boot 3.4 und Java 21 erstellen. Jeder Schritt enthält vollständigen, copy-paste-fähigen Code. Am Ende haben Sie eine funktionierende API mit CRUD-Endpunkten, Validierung, Fehlerbehandlung, OpenAPI-Dokumentation, Tests und Docker-Deployment.

Voraussetzungen

Bevor wir starten, stellen Sie sicher, dass folgende Tools installiert sind:

  • Java 21 (LTS): java -version sollte 21.x anzeigen. Download: adoptium.net
  • Maven 3.9+ oder Gradle 8.x: Wir verwenden Maven in diesem Tutorial.
  • IDE: IntelliJ IDEA (empfohlen) oder VS Code mit Java Extension Pack.
  • Docker Desktop: Für Schritt 7 (Deployment). Optional für die ersten 6 Schritte.

Die 7 Schritte im Überblick

Spring Boot REST API: 7 Schritte von der Idee bis zum Deployment1InitializrProjekt2JPA-EntityDatenmodell3RepositorySpring Data4ControllerCRUD-Endp.5ValidierungDTOs + Errors6OpenAPISwagger7Tests +DockerDaten-Schicht (Model + Repository)Web-Schicht (Controller + DTOs)Ops (Docs + Deploy)Tech-Stack:Java 21Spring Boot 3.4Spring Data JPAMavenDockerJUnit 5Zeitaufwand: ~4 Stunden (Grundimplementierung) | 1–2 Tage (produktionsreif mit Tests & Docker)

Schritt 1: Spring Boot Projekt mit Spring Initializr erstellen

Der schnellste Weg zu einem neuen Spring-Boot-Projekt führt über Spring Initializr. Sie können das Projekt entweder über die Web-Oberfläche oder direkt per cURL erstellen.

Option A: Web-Oberfläche (start.spring.io)

Konfigurieren Sie folgende Einstellungen:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.4.x (neueste stabile Version)
  • Group: de.meinefirma
  • Artifact: produkt-api
  • Java: 21
  • Dependencies: Spring Web, Spring Data JPA, H2 Database, Validation, Spring Boot DevTools

Option B: cURL (Terminal)

curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.4.1 \
  -d groupId=de.meinefirma \
  -d artifactId=produkt-api \
  -d name=produkt-api \
  -d javaVersion=21 \
  -d dependencies=web,data-jpa,h2,validation,devtools \
  -o produkt-api.zip

unzip produkt-api.zip -d produkt-api
cd produkt-api

Nach dem Entpacken haben Sie folgende Projektstruktur:

produkt-api/
├── src/
│   ├── main/
│   │   ├── java/de/meinefirma/produktapi/
│   │   │   └── ProduktApiApplication.java
│   │   └── resources/
│   │       └── application.properties
│   └── test/
│       └── java/de/meinefirma/produktapi/
│           └── ProduktApiApplicationTests.java
├── pom.xml
└── mvnw

Starten Sie die Anwendung mit ./mvnw spring-boot:run. Wenn Sie auf http://localhost:8080 eine 404-Seite sehen, funktioniert alles — es gibt nur noch keine Endpunkte.

Schritt 2: Datenmodell und JPA-Entities definieren

Wir bauen eine Produkt-API — ein klassisches Beispiel, das sich auf jede Domäne übertragen lässt. Erstellen Sie die Entity-Klasse Produkt.java:

package de.meinefirma.produktapi.model;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "produkte")
public class Produkt {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String name;

    @Column(length = 2000)
    private String beschreibung;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal preis;

    @Column(nullable = false)
    private String kategorie;

    @Column(nullable = false)
    private Integer bestand;

    @Column(name = "erstellt_am", updatable = false)
    private LocalDateTime erstelltAm;

    @Column(name = "aktualisiert_am")
    private LocalDateTime aktualisiertAm;

    @PrePersist
    protected void onCreate() {
        erstelltAm = LocalDateTime.now();
        aktualisiertAm = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        aktualisiertAm = LocalDateTime.now();
    }

    // Konstruktoren
    public Produkt() {}

    public Produkt(String name, String beschreibung,
                   BigDecimal preis, String kategorie, Integer bestand) {
        this.name = name;
        this.beschreibung = beschreibung;
        this.preis = preis;
        this.kategorie = kategorie;
        this.bestand = bestand;
    }

    // Getter und Setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getBeschreibung() { return beschreibung; }
    public void setBeschreibung(String beschreibung) {
        this.beschreibung = beschreibung;
    }
    public BigDecimal getPreis() { return preis; }
    public void setPreis(BigDecimal preis) { this.preis = preis; }
    public String getKategorie() { return kategorie; }
    public void setKategorie(String kategorie) {
        this.kategorie = kategorie;
    }
    public Integer getBestand() { return bestand; }
    public void setBestand(Integer bestand) { this.bestand = bestand; }
    public LocalDateTime getErstelltAm() { return erstelltAm; }
    public LocalDateTime getAktualisiertAm() { return aktualisiertAm; }
}

Konfigurieren Sie die H2-Datenbank in application.properties:

# Datenbank-Konfiguration
spring.datasource.url=jdbc:h2:mem:produktdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# Server
server.port=8080

Warum H2? Für die Entwicklung ist eine In-Memory-Datenbank ideal — kein Setup nötig, die Konsole unter /h2-console ermöglicht SQL-Debugging. Für Produktion ersetzen Sie H2 durch PostgreSQL oder MySQL (nur die application.properties ändern).

Schritt 3: Repository-Schicht mit Spring Data JPA implementieren

Spring Data JPA generiert die gesamte CRUD-Logik automatisch. Sie müssen lediglich ein Interface definieren:

package de.meinefirma.produktapi.repository;

import de.meinefirma.produktapi.model.Produkt;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;

@Repository
public interface ProduktRepository extends JpaRepository<Produkt, Long> {

    // Derived Query: Spring generiert SQL automatisch
    List<Produkt> findByKategorie(String kategorie);

    List<Produkt> findByNameContainingIgnoreCase(String name);

    List<Produkt> findByPreisBetween(BigDecimal min, BigDecimal max);

    // Custom JPQL-Query
    @Query("SELECT p FROM Produkt p WHERE p.bestand < :schwelle")
    List<Produkt> findNiedrigerBestand(Integer schwelle);

    // Statistik
    @Query("SELECT COUNT(p) FROM Produkt p WHERE p.kategorie = :kategorie")
    long zaehleNachKategorie(String kategorie);
}

Das ist der gesamte Datenzugriffscode. Spring Data JPA generiert die Implementierung zur Laufzeit. Die Methode findByKategorie wird automatisch zu SELECT * FROM produkte WHERE kategorie = ? übersetzt. Für komplexere Abfragen nutzen Sie @Query mit JPQL.

Schritt 4: REST-Controller mit CRUD-Endpunkten erstellen

Jetzt verbinden wir alles in einem REST-Controller. Wir folgen dem Service-Pattern: Controller → Service → Repository.

Zuerst der Service:

package de.meinefirma.produktapi.service;

import de.meinefirma.produktapi.model.Produkt;
import de.meinefirma.produktapi.repository.ProduktRepository;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;

@Service
public class ProduktService {

    private final ProduktRepository repository;

    public ProduktService(ProduktRepository repository) {
        this.repository = repository;
    }

    public List<Produkt> alleProdukte() {
        return repository.findAll();
    }

    public Produkt produktNachId(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new ProduktNichtGefundenException(
                "Produkt mit ID " + id + " nicht gefunden"));
    }

    public Produkt erstellen(Produkt produkt) {
        return repository.save(produkt);
    }

    public Produkt aktualisieren(Long id, Produkt details) {
        Produkt produkt = produktNachId(id);
        produkt.setName(details.getName());
        produkt.setBeschreibung(details.getBeschreibung());
        produkt.setPreis(details.getPreis());
        produkt.setKategorie(details.getKategorie());
        produkt.setBestand(details.getBestand());
        return repository.save(produkt);
    }

    public void loeschen(Long id) {
        Produkt produkt = produktNachId(id);
        repository.delete(produkt);
    }

    public List<Produkt> nachKategorie(String kategorie) {
        return repository.findByKategorie(kategorie);
    }

    public List<Produkt> suchen(String name) {
        return repository.findByNameContainingIgnoreCase(name);
    }
}

Die Custom Exception:

package de.meinefirma.produktapi.service;

public class ProduktNichtGefundenException extends RuntimeException {
    public ProduktNichtGefundenException(String nachricht) {
        super(nachricht);
    }
}

Und der REST-Controller:

package de.meinefirma.produktapi.controller;

import de.meinefirma.produktapi.model.Produkt;
import de.meinefirma.produktapi.service.ProduktService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/v1/produkte")
public class ProduktController {

    private final ProduktService service;

    public ProduktController(ProduktService service) {
        this.service = service;
    }

    @GetMapping
    public List<Produkt> alleProdukte() {
        return service.alleProdukte();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Produkt> produktNachId(@PathVariable Long id) {
        return ResponseEntity.ok(service.produktNachId(id));
    }

    @PostMapping
    public ResponseEntity<Produkt> erstellen(@RequestBody Produkt produkt) {
        Produkt gespeichert = service.erstellen(produkt);
        return ResponseEntity.status(HttpStatus.CREATED).body(gespeichert);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Produkt> aktualisieren(
            @PathVariable Long id, @RequestBody Produkt details) {
        return ResponseEntity.ok(service.aktualisieren(id, details));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> loeschen(@PathVariable Long id) {
        service.loeschen(id);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/kategorie/{kategorie}")
    public List<Produkt> nachKategorie(@PathVariable String kategorie) {
        return service.nachKategorie(kategorie);
    }

    @GetMapping("/suche")
    public List<Produkt> suchen(@RequestParam String name) {
        return service.suchen(name);
    }
}

Testen Sie die API mit cURL:

# Produkt erstellen
curl -X POST http://localhost:8080/api/v1/produkte \
  -H "Content-Type: application/json" \
  -d '{"name":"MacBook Pro 16","beschreibung":"Apple M4 Max, 64GB RAM","preis":4299.00,"kategorie":"Laptops","bestand":25}'

# Alle Produkte abrufen
curl http://localhost:8080/api/v1/produkte

# Produkt nach ID
curl http://localhost:8080/api/v1/produkte/1

# Suchen
curl "http://localhost:8080/api/v1/produkte/suche?name=macbook"

Schritt 5: Validierung, Fehlerbehandlung und DTOs hinzufügen

Produktionsreife APIs validieren Eingaben und geben strukturierte Fehlermeldungen zurück. Wir verwenden Bean Validation (Jakarta Validation) und trennen Request-DTOs von Entities.

Erstellen Sie das Request-DTO als Java Record:

package de.meinefirma.produktapi.dto;

import jakarta.validation.constraints.*;
import java.math.BigDecimal;

public record ProduktRequest(
    @NotBlank(message = "Name darf nicht leer sein")
    @Size(min = 2, max = 200, message = "Name muss 2-200 Zeichen lang sein")
    String name,

    @Size(max = 2000, message = "Beschreibung darf max. 2000 Zeichen lang sein")
    String beschreibung,

    @NotNull(message = "Preis ist erforderlich")
    @DecimalMin(value = "0.01", message = "Preis muss mindestens 0.01 sein")
    @DecimalMax(value = "999999.99", message = "Preis darf max. 999999.99 sein")
    BigDecimal preis,

    @NotBlank(message = "Kategorie darf nicht leer sein")
    String kategorie,

    @NotNull(message = "Bestand ist erforderlich")
    @Min(value = 0, message = "Bestand darf nicht negativ sein")
    Integer bestand
) {}

Der globale Exception-Handler fängt alle Fehler ab und gibt einheitliche JSON-Antworten zurück:

package de.meinefirma.produktapi.exception;

import de.meinefirma.produktapi.service.ProduktNichtGefundenException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProduktNichtGefundenException.class)
    public ResponseEntity<Map<String, Object>> handleNichtGefunden(
            ProduktNichtGefundenException ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("zeitstempel", LocalDateTime.now());
        body.put("status", 404);
        body.put("fehler", "Nicht gefunden");
        body.put("nachricht", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidierung(
            MethodArgumentNotValidException ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("zeitstempel", LocalDateTime.now());
        body.put("status", 400);
        body.put("fehler", "Validierungsfehler");

        List<Map<String, String>> fehler = ex.getBindingResult()
            .getFieldErrors().stream()
            .map(f -> Map.of(
                "feld", f.getField(),
                "nachricht", Objects.requireNonNullElse(
                    f.getDefaultMessage(), "Ungültiger Wert")
            )).toList();

        body.put("details", fehler);
        return ResponseEntity.badRequest().body(body);
    }
}

Aktualisieren Sie den Controller, um @Valid und das DTO zu verwenden:

@PostMapping
public ResponseEntity<Produkt> erstellen(
        @Valid @RequestBody ProduktRequest request) {
    Produkt produkt = new Produkt(
        request.name(),
        request.beschreibung(),
        request.preis(),
        request.kategorie(),
        request.bestand()
    );
    Produkt gespeichert = service.erstellen(produkt);
    return ResponseEntity.status(HttpStatus.CREATED).body(gespeichert);
}

Jetzt erhalten Sie bei ungültigen Eingaben strukturierte Fehlermeldungen:

{
  "zeitstempel": "2026-06-07T10:30:00",
  "status": 400,
  "fehler": "Validierungsfehler",
  "details": [
    { "feld": "name", "nachricht": "Name darf nicht leer sein" },
    { "feld": "preis", "nachricht": "Preis muss mindestens 0.01 sein" }
  ]
}

Schritt 6: API-Dokumentation mit SpringDoc OpenAPI generieren

Professionelle APIs brauchen Dokumentation. SpringDoc OpenAPI generiert automatisch eine interaktive Swagger-UI aus Ihren Controller-Annotationen.

Fügen Sie die Dependency in pom.xml hinzu:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.8.4</version>
</dependency>

Konfigurieren Sie OpenAPI in application.properties:

# OpenAPI
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.operationsSorter=method

Ergänzen Sie den Controller mit OpenAPI-Annotationen für detaillierte Beschreibungen:

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "Produkte", description = "CRUD-Operationen für Produkte")
@RestController
@RequestMapping("/api/v1/produkte")
public class ProduktController {

    @Operation(
        summary = "Alle Produkte abrufen",
        description = "Gibt eine Liste aller Produkte zurück"
    )
    @ApiResponse(responseCode = "200", description = "Erfolgreiche Abfrage")
    @GetMapping
    public List<Produkt> alleProdukte() {
        return service.alleProdukte();
    }

    @Operation(summary = "Produkt nach ID abrufen")
    @ApiResponse(responseCode = "200", description = "Produkt gefunden")
    @ApiResponse(responseCode = "404", description = "Produkt nicht gefunden")
    @GetMapping("/{id}")
    public ResponseEntity<Produkt> produktNachId(
        @Parameter(description = "Produkt-ID") @PathVariable Long id) {
        return ResponseEntity.ok(service.produktNachId(id));
    }
}

Starten Sie die Anwendung und öffnen Sie http://localhost:8080/swagger-ui.html. Sie sehen eine vollständige, interaktive API-Dokumentation mit Try-it-out-Funktion — ohne eine einzige Zeile manueller Dokumentation.

Spring Boot REST API: Schichtarchitektur

Spring Boot REST API: Schichtarchitektur (Layered Architecture)HTTP-Client (cURL / Frontend)JSONController-Schicht@RestController, @GetMapping, @PostMapping, @ValidDTOsService-Schicht@Service, Geschäftslogik, Validierung, TransaktionenEntitiesRepository-SchichtJpaRepository, @Query, Derived QueriesJDBCH2 / PostgreSQL / MySQLSpring Boot 3.4 | Java 21 | Spring Data JPA | Bean Validation

Schritt 7: Tests schreiben und in Docker deployen

Professionelle APIs werden getestet und containerisiert. Wir schreiben Unit-Tests für den Service und Integrationstests für den Controller.

Unit-Test für den Service (JUnit 5 + Mockito)

package de.meinefirma.produktapi.service;

import de.meinefirma.produktapi.model.Produkt;
import de.meinefirma.produktapi.repository.ProduktRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class ProduktServiceTest {

    @Mock
    private ProduktRepository repository;

    @InjectMocks
    private ProduktService service;

    @Test
    void sollteProduktNachIdFinden() {
        // Given
        Produkt produkt = new Produkt("Laptop", "Test",
            new BigDecimal("999.99"), "Elektronik", 10);
        produkt.setId(1L);
        when(repository.findById(1L)).thenReturn(Optional.of(produkt));

        // When
        Produkt ergebnis = service.produktNachId(1L);

        // Then
        assertThat(ergebnis.getName()).isEqualTo("Laptop");
        assertThat(ergebnis.getPreis())
            .isEqualByComparingTo(new BigDecimal("999.99"));
        verify(repository, times(1)).findById(1L);
    }

    @Test
    void sollteExceptionWerfenWennNichtGefunden() {
        // Given
        when(repository.findById(99L)).thenReturn(Optional.empty());

        // When & Then
        assertThatThrownBy(() -> service.produktNachId(99L))
            .isInstanceOf(ProduktNichtGefundenException.class)
            .hasMessageContaining("99");
    }

    @Test
    void sollteNeuesProduktErstellen() {
        // Given
        Produkt neuesProdukt = new Produkt("Monitor", "27 Zoll 4K",
            new BigDecimal("549.00"), "Monitore", 50);
        when(repository.save(neuesProdukt)).thenReturn(neuesProdukt);

        // When
        Produkt gespeichert = service.erstellen(neuesProdukt);

        // Then
        assertThat(gespeichert.getName()).isEqualTo("Monitor");
        verify(repository).save(neuesProdukt);
    }
}

Integrationstest für den Controller

package de.meinefirma.produktapi.controller;

import de.meinefirma.produktapi.model.Produkt;
import de.meinefirma.produktapi.repository.ProduktRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProduktControllerIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ProduktRepository repository;

    @BeforeEach
    void setUp() {
        repository.deleteAll();
    }

    @Test
    void sollteProduktErstellenUndAbrufen() {
        // Erstellen
        Produkt produkt = new Produkt("Tastatur", "Mechanisch",
            new BigDecimal("149.99"), "Zubehör", 100);
        ResponseEntity<Produkt> createResponse =
            restTemplate.postForEntity("/api/v1/produkte",
                produkt, Produkt.class);

        assertThat(createResponse.getStatusCode())
            .isEqualTo(HttpStatus.CREATED);
        assertThat(createResponse.getBody()).isNotNull();
        assertThat(createResponse.getBody().getId()).isNotNull();

        // Abrufen
        Long id = createResponse.getBody().getId();
        ResponseEntity<Produkt> getResponse =
            restTemplate.getForEntity("/api/v1/produkte/" + id,
                Produkt.class);

        assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getResponse.getBody().getName()).isEqualTo("Tastatur");
    }
}

Tests ausführen: ./mvnw test

Docker-Deployment mit Multi-Stage Build

Erstellen Sie ein Dockerfile im Projektroot:

# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY pom.xml .
COPY mvnw .
COPY .mvn .mvn
RUN ./mvnw dependency:resolve
COPY src src
RUN ./mvnw package -DskipTests

# Stage 2: Run
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Und eine docker-compose.yml für Produktion mit PostgreSQL:

version: '3.9'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/produktdb
      - SPRING_DATASOURCE_USERNAME=postgres
      - SPRING_DATASOURCE_PASSWORD=sicheres-passwort
      - SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: produktdb
      POSTGRES_PASSWORD: sicheres-passwort
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

Starten Sie alles mit:

docker compose up --build -d

# API testen
curl http://localhost:8080/api/v1/produkte

# Logs ansehen
docker compose logs -f app

Zusammenfassung: Ihre REST API ist produktionsbereit

Sie haben in 7 Schritten eine vollständige REST API mit Spring Boot erstellt:

  1. Projekt erstellt mit Spring Initializr (Java 21, Spring Boot 3.4)
  2. Datenmodell definiert mit JPA-Entity und Lifecycle-Callbacks
  3. Repository implementiert mit Spring Data JPA und Custom Queries
  4. CRUD-Controller gebaut mit RESTful-Endpunkten und ResponseEntity
  5. Validierung hinzugefügt mit Bean Validation, DTOs und globalem Error-Handler
  6. API dokumentiert mit SpringDoc OpenAPI und Swagger-UI
  7. Tests geschrieben (Unit + Integration) und Docker-Deployment konfiguriert

Die API ist bereit für den nächsten Schritt: Authentifizierung mit Spring Security und JWT, Caching mit Redis, Rate Limiting, oder die Migration auf eine reaktive Architektur mit Spring WebFlux.

Spring-Boot-Entwickler für Ihr Team?

Wir helfen deutschen Unternehmen, erfahrene Java/Spring-Boot-Entwickler in 21 Tagen einzustellen. Zugang zu 500+ vorgeprüften Backend-Profilen in DACH. Senior-Level ab €78.000.

30-Minuten-Beratung buchen →

Verwandte Artikel

Häufig gestellte Fragen (FAQ)

Welche Java-Version sollte ich für Spring Boot 2026 verwenden?

Java 21 (LTS) ist die empfohlene Version für Spring Boot 3.4 in 2026. Java 21 bietet Virtual Threads (Project Loom) für bessere Skalierung, Pattern Matching für prägnantere Switch-Ausdrücke, Record Patterns und Sequenced Collections. Spring Boot 3.4 setzt mindestens Java 17 voraus, aber Java 21 ist der Standard für neue Projekte. Wenn Sie GraalVM Native Image nutzen wollen, ist Java 21 ebenfalls die beste Wahl.

Was ist der Unterschied zwischen Spring Boot und Spring Framework?

Das Spring Framework ist das Basis-Framework mit Dependency Injection, AOP (Aspect-Oriented Programming) und Kernmodulen wie Spring MVC, Spring Data und Spring Security. Spring Boot ist eine Erweiterung, die Auto-Configuration, eingebettete Server (Tomcat, Jetty, Undertow), Starter-Dependencies und Production-Ready-Features (Actuator für Health Checks, Metriken) hinzufügt. In der Praxis: Mit Spring Boot erstellen Sie in Minuten eine lauffähige Anwendung, die Konfiguration erfolgt über application.yml statt über XML-Dateien. Über 95% der neuen Spring-Projekte nutzen Spring Boot.

Soll ich Maven oder Gradle für Spring Boot verwenden?

Beide Build-Tools sind vollständig unterstützt. Maven ist in deutschen Unternehmen und Enterprise-Projekten weiter verbreitet — über 65% der Spring-Boot-Projekte in DACH nutzen Maven. Gradle bietet schnellere Builds durch inkrementelle Kompilierung und Build-Cache und ist bei Android- und Kotlin-Projekten Standard. Für Einsteiger empfehle ich Maven wegen der besseren Dokumentation und der breiteren Community-Unterstützung im deutschsprachigen Raum. Für große Monorepos oder Performance-kritische CI/CD-Pipelines kann Gradle 20–40% Build-Zeit sparen.

Wie viel verdient ein Java/Spring-Boot-Entwickler in Deutschland 2026?

Junior Java/Spring-Boot-Entwickler verdienen 2026 zwischen 45.000 und 58.000 Euro brutto pro Jahr. Mid-Level mit 3–5 Jahren Erfahrung liegen bei 58.000 bis 78.000 Euro. Senior-Entwickler mit 5+ Jahren Spring-Boot-Erfahrung, Microservices und Cloud-Kenntnissen verdienen 78.000 bis 105.000 Euro. In München und Frankfurt liegen die Gehälter 10–15% über dem Bundesdurchschnitt. Für Freelancer liegt der Tagessatz bei 600–950 Euro netto. Spring-Boot-Entwickler mit zusätzlicher Kubernetes- oder KI-Erfahrung können bis zu 15% Gehaltszuschlag erwarten.

Java-Entwickler für Ihr nächstes Projekt?

Von Junior bis Staff — wir vermitteln erfahrene Java/Spring-Boot-Entwickler an deutsche Unternehmen. 21-Tage-Pipeline, 500+ vorgeprüfte Profile, Equity-Beratung inklusive.

Jetzt Beratungstermin buchen →

Geschrieben von Anna Schneider, Backend-Entwicklerin mit 8 Jahren Spring-Boot-Erfahrung in Enterprise-Projekten bei SAP, Deutsche Bank und mehreren Berliner Startups. Letztes Update: 7. Juni 2026.