Zum Inhalt

Beispielarbeiten · Code & Fehler

Spring Boot: REST-Schnittstelle mit Datenbank, Validierung und Tests

Eine Aufgaben-API mit Spring Boot und Maven: Anlegen, Lesen, Ändern und Löschen, Eingabeprüfung mit klaren Fehlermeldungen. Geprüft mit JUnit und mit echten HTTP-Aufrufen, dazu eine Gegenprobe mit kaputten Fassungen.

Musterarbeit (Demo), kein Kundenauftrag. Gebaut als Probeaufgabe mit erfundenen Beispieldaten; die Prüfzahlen unten stammen aus dem automatischen Prüflauf und sind nicht geschönt.

Was gebaut wurde

  • Controller, Repository und Fehlerbehandlung getrennt, Antworten mit passenden Statuscodes
  • Eingaben werden geprüft, Fehler kommen als lesbare Meldung zurück
  • JUnit-Tests und Aufrufe gegen den laufenden Server
  • Gegenprobe: jede absichtlich kaputte Fassung wird von den Prüfungen erkannt

spring-boot-api-quellcode.zip herunterladen

Spring Boot 3 mit Java 21 und Maven; Spring Boot 2, ältere Java-Versionen und Gradle sind nicht geprüft. Die Datenbank ist H2 im Speicher, PostgreSQL oder MySQL liefen nicht. Ohne Anmeldung (Spring Security, JWT), ohne Microservices und ohne Docker. Geliefert wird Quellcode mit Testnachweis, gearbeitet wird nicht auf dem Server des Kunden.

Wie geprüft wurde

Automatischer Prüflauf vom 01.11.2026, aus dem Protokoll (Testdaten erfunden, Zahlen gemessen):

  • mvn test 5 JUnit-Tests, 0 rot
  • HTTP 21/21 gruen
  • Gegenprobe 3/3 kaputte Fassungen von JUnit UND HTTP-Pruefung erkannt

Grenzen

Was diese Probe nicht abdeckt, steht hier, damit niemand mehr erwartet:

  • Spring Boot 3.3 / Java 21 / Maven 3.9: andere Hauptversionen (Spring Boot 2.x mit javax, Java 8/11, Gradle) nicht geprueft
  • Datenbank nur H2 im Speicher; PostgreSQL/MySQL/Oracle nicht gelaufen (Kundencode mit anderer DB wird nicht gegen sie getestet)
  • keine Authentifizierung (Spring Security, JWT, OAuth), keine Microservices/Messaging, kein Docker/Kubernetes
  • Maven-Abhaengigkeiten kommen aus Maven Central (Netz noetig beim ersten Lauf); Kundenprojekte mit privatem Repository brauchen Zugang
  • keine Arbeit auf dem Server des Kunden ohne Zugang; Ergebnis ist Quellcode + Testnachweis

Der Controller

package de.gltools.aufgaben;

import jakarta.validation.Valid;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/aufgaben")
public class AufgabenController {
    private final AufgabenRepo repo;

    public AufgabenController(AufgabenRepo repo) { this.repo = repo; }

    @GetMapping
    public List<Aufgabe> liste() { return repo.findAll(); }

    @GetMapping("/{id}")
    public ResponseEntity<Aufgabe> eine(@PathVariable Long id) {
        return repo.findById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Aufgabe neu(@Valid @RequestBody Aufgabe a) {
        a.setId(null);
        return repo.save(a);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Aufgabe> aendern(@PathVariable Long id, @Valid @RequestBody Aufgabe a) {
        if (!repo.existsById(id)) return ResponseEntity.notFound().build();
        a.setId(id);
        return ResponseEntity.ok(repo.save(a));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> loeschen(@PathVariable Long id) {
        if (!repo.existsById(id)) return ResponseEntity.notFound().build();
        repo.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

Die Tests

package de.gltools.aufgaben;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class AufgabenTest {
    @Autowired MockMvc mvc;

    private String anlegen(String json) throws Exception {
        return mvc.perform(post("/api/aufgaben").contentType(MediaType.APPLICATION_JSON).content(json))
                .andExpect(status().isCreated()).andReturn().getResponse().getContentAsString();
    }

    @Test
    void anlegenUndLesen() throws Exception {
        anlegen("{\"titel\":\"Test lesen\",\"prioritaet\":2}");
        mvc.perform(get("/api/aufgaben")).andExpect(status().isOk())
                .andExpect(jsonPath("$[*].titel", hasItem("Test lesen")));
    }

    @Test
    void leererTitelIst400() throws Exception {
        mvc.perform(post("/api/aufgaben").contentType(MediaType.APPLICATION_JSON).content("{\"titel\":\"  \"}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.felder.titel", is("Titel fehlt")));
    }

    @Test
    void prioritaetAusserhalbIst400() throws Exception {
        for (int p : new int[] {0, 6}) {
            mvc.perform(post("/api/aufgaben").contentType(MediaType.APPLICATION_JSON)
                    .content("{\"titel\":\"x\",\"prioritaet\":" + p + "}"))
                    .andExpect(status().isBadRequest()).andExpect(jsonPath("$.felder.prioritaet").exists());
        }
    }

    @Test
    void unbekannteIdIst404() throws Exception {
        mvc.perform(get("/api/aufgaben/999999")).andExpect(status().isNotFound());
        mvc.perform(delete("/api/aufgaben/999999")).andExpect(status().isNotFound());
        mvc.perform(put("/api/aufgaben/999999").contentType(MediaType.APPLICATION_JSON).content("{\"titel\":\"gibt es nicht\"}"))
                .andExpect(status().isNotFound());
        mvc.perform(get("/api/aufgaben")).andExpect(jsonPath("$[*].titel", not(hasItem("gibt es nicht"))));
    }

    @Test
    void aendernUndLoeschen() throws Exception {
        String body = anlegen("{\"titel\":\"alt\"}");
        String id = body.replaceAll(".*\"id\":(\\d+).*", "$1");
        mvc.perform(put("/api/aufgaben/" + id).contentType(MediaType.APPLICATION_JSON)
                .content("{\"titel\":\"neu\",\"erledigt\":true,\"prioritaet\":1}"))
                .andExpect(status().isOk()).andExpect(jsonPath("$.titel", is("neu"))).andExpect(jsonPath("$.erledigt", is(true)));
        mvc.perform(delete("/api/aufgaben/" + id)).andExpect(status().isNoContent());
        mvc.perform(get("/api/aufgaben/" + id)).andExpect(status().isNotFound());
    }
}

So etwas für deinen Betrieb?

Beschreib kurz, was du brauchst. Du bekommst einen Festpreis, bevor ich anfange; ohne dein Ja passiert nichts.

Zur Werkstatt