A small Spring Boot REST API backed by PostgreSQL running in Docker Compose, with
an Nginx reverse proxy enforcing HTTPS when run via the full Docker Compose stack.
Built while following the Amigoscode Spring Boot course.
GET /api/v1/software-engineers returns software engineers stored in Postgres,
mapped with Spring Data JPA.
Tech stack
Spring Boot 4 REST API with Spring Data JPA, backed by Postgres running in Docker Compose wrapped with Spring AI MCP Server layer
2026-10-28
ดาว
1
เครื่องมือ
—
อัปไทม์
—
Component
Version
Java
21
Spring Boot
4.1.1
Spring Data JPA (Hibernate)
via spring-boot-starter-data-jpa
Spring AI (MCP server)
2.0.1 — spring-ai-starter-mcp-server-webmvc
PostgreSQL
18 (Docker image postgres:latest)
Nginx (reverse proxy, HTTPS)
1.27-alpine — full Docker Compose profile only
Build
Maven Wrapper 3.9.16
Prerequisites
JDK 21
Docker with Compose v2 (docker compose, not docker-compose)
No local Maven or Postgres install is needed — the Maven Wrapper and Docker cover both.
Quick start
git clone https://github.com/OmarNizam/spring-boot.git
cd spring-boot
./mvnw spring-boot:run
That's it. Spring Boot's Docker Compose support reads compose.yaml, starts the
Postgres container automatically, and points the datasource at it. Hibernate reconciles the
schema with the entity classes on startup (ddl-auto=update) and, on a fresh database,
DataSeeder inserts two sample engineers.
id is a server-generated UUID, so the exact values differ on every run.
Running
Option 1 — spring-boot:run or your IDE (recommended for development)
./mvnw spring-boot:run
Postgres is started for you and left running when the app stops
(spring.docker.compose.lifecycle-management=start-only).
Option 2 — executable jar
docker compose up -d db # start Postgres yourself first
./mvnw package
java -jar target/spring-boot-0.0.1-SNAPSHOT.jar
Docker Compose support is deliberately excluded from the repackaged jar by
spring-boot-maven-plugin, so the jar does not start Postgres — it connects using the
spring.datasource.* properties in application.properties. Start the database first.
Option 3 — everything in Docker
./docker/nginx/generate-certs.sh # once per clone — self-signed cert, gitignored
docker compose --profile full up --build
The app service sits behind the full compose profile. Without that flag,
docker compose up starts only the database — which is what you want during development,
so the app image isn't rebuilt on every run.
This is also the only mode where HTTPS is enforced: an nginx reverse proxy
terminates TLS on https://localhost (port 443) using the cert generated
above, redirects any plain http://localhost request to HTTPS (308), and
is the only way in — app's container port is no longer published to the
host, so it can't be reached directly. Since the cert is self-signed, curl
needs -k and browsers/API clients need to accept the certificate warning
once:
The http://localhost:8080 URLs elsewhere in this README are for Options 1
and 2 (local spring-boot:run / jar), which remain plain HTTP — nginx only
exists inside the full Compose stack.
API
Base URL: http://localhost:8080
Method
Path
Description
Response
GET
/api/v1/software-engineers
All software engineers
200 — JSON array
GET
/api/v1/software-engineers/{id}
One software engineer by id
200 — JSON object; 404 if no such id; 400 if {id} is not a UUID
POST
/api/v1/software-engineers
Create a software engineer
201 — created entity, Location header; 400 on a validation failure
PUT
/api/v1/software-engineers/{id}
Replace a software engineer's name/techStack by id
200 — updated entity; 404 if no such id; 400 on a validation failure or if {id} is not a UUID
DELETE
/api/v1/software-engineers/{id}
Delete a software engineer by id
204 — no body; 404 if no such id; 400 if {id} is not a UUID
requests.http in the project root contains this call, runnable directly from
IntelliJ's HTTP client.
SoftwareEngineer
Field
Type
Notes
id
UUID
Generated by Hibernate (@GeneratedValue(strategy = UUID)) on persist
name
string
techStack
array of strings
Stored in the software_engineer_tech_stack side table
MCP server
The same five operations are also exposed to a Model Context Protocol client at
POST http://localhost:8080/mcp (Streamable-HTTP, same port and servlet context as
the REST API — no separate connector). The SoftwareEngineerService bean is shared,
so an MCP call and the matching REST call have identical persistence semantics. The
surface is unauthenticated and includes the write operations — see
docs/ARCHITECTURE.md "The MCP server".
Tool
What it does
list-software-engineers
Every engineer, each with id, name, and tech stack
get-software-engineer
One engineer by id (UUID); errors if unknown
create-software-engineer
Create from name + techStack; returns it with its generated id
update-software-engineer
Full replace of an engineer's name + techStack by id
delete-software-engineer
Delete by id; errors if unknown
Try it
MCP Inspector — nothing to hand-assemble:
npx @modelcontextprotocol/inspector
Then connect to http://localhost:8080/mcp with transport Streamable HTTP
(or https://localhost/mcp when running Option 3 — MCP Inspector is
browser-based, so accept the self-signed certificate warning by opening
https://localhost/mcp directly in the browser first, or configure the
client to ignore TLS errors).
From an MCP client (.mcp.json, Claude Code, and similar):
Then send that mcp-session-id back on every subsequent call:
curl -X POST http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'mcp-session-id: <value from the initialize response>' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
Responses arrive either as plain JSON or as an SSE data: frame, depending on the call.
Database
Setting
Value
Host port
5332 (container port 5432)
Database
postgres-spring-boot
User / password
amigoscode / password
Container
postgres-spring-boot
Volume
postgres_data (survives docker compose down; with ddl-auto=update the rows persist across app restarts too — see below)
The credentials above are local development values, committed intentionally so the
project runs after a clone. Do not reuse this setup as-is for anything deployed.
Inspect the data directly:
docker exec -it postgres-spring-boot psql -U amigoscode -d postgres-spring-boot
\dt
select * from software_engineer;
select * from software_engineer_tech_stack;
Schema is managed by Hibernate (spring.jpa.hibernate.ddl-auto=update) — on startup it
adds any missing tables and columns from the entity classes and leaves existing data in
place. There are no migration files, but rows now survive an app restart because the
postgres_data volume keeps them.
DataSeeder therefore seeds only a fresh database; on later runs its count() > 0 guard
skips it. To wipe the persisted volume (e.g. after changing the Postgres image or to force
a re-seed):
docker compose down -v && ./mvnw spring-boot:run
Configuration
All settings live in src/main/resources/application.properties.
Keeps the exception trace out of JSON error bodies (devtools would otherwise force always in dev). Boot 4 renamed this from server.error.*
spring.ai.mcp.server.protocol
STREAMABLE
Selects Streamable-HTTP (POST /mcp). Must be explicit — the webmvc starter defaults to the deprecated SSE transport when unset
spring.ai.mcp.server.name / .version
software-engineers-mcp / 0.0.1
Server identity reported to MCP clients
spring.ai.mcp.server.instructions
(multi-line)
Usage blurb the server hands the client on connect
Project structure
src/main/java/com/amigoscode/
├── Application.java # @SpringBootApplication entry point
├── SoftwareEngineer.java # JPA entity (UUID id)
├── CreateSoftwareEngineerRequest.java # POST request body (no id field) + validation
├── UpdateSoftwareEngineerRequest.java # PUT request body (no id field) + validation
├── SoftwareEngineerRepository.java # JpaRepository<SoftwareEngineer, UUID>
├── SoftwareEngineerService.java # @Service — business logic between controller and repository
├── SoftwareEngineerController.java # REST controller (/api/v1/software-engineers)
├── SoftwareEngineerMcpTools.java # MCP tools component (/mcp) — second adapter over the service
├── SoftwareEngineerNotFoundException.java # @ResponseStatus(404) — thrown on an unknown id
└── DataSeeder.java # Seeds sample data when the table is empty
compose.yaml # Postgres (+ app and nginx behind the "full" profile)
Dockerfile # Multi-stage build of the application image
docker/nginx/conf.d/default.conf # Nginx reverse-proxy config (TLS termination, HTTP→HTTPS redirect)
docker/nginx/generate-certs.sh # Generates a local self-signed cert (gitignored output)
requests.http # Ready-made HTTP requests
docs/ARCHITECTURE.md # Design decisions and how the pieces fit
Tests
./mvnw test
Test
Type
Needs Postgres?
ApplicationTests.contextLoads
@SpringBootTest — boots the full context
Yes
SoftwareEngineerRepositoryTest
@SpringBootTest + @Transactional — JPA mapping (UUID generation, techStack side table) against real Postgres, rolled back
Yes
SoftwareEngineerServiceIntegrationTest
@SpringBootTest + @Transactional — create/update/delete through the real service and Hibernate against Postgres, rolled back
Yes
SoftwareEngineerMcpIntegrationTest
@SpringBootTest + MCP client — drives the running MCP server end to end
Yes
SoftwareEngineerControllerTest
@WebMvcTest slice — MockMvc against the controller with the service mocked
No
SoftwareEngineerServiceTest
Plain Mockito unit test — service with the repository mocked
No
SoftwareEngineerMcpToolsTest
Plain Mockito unit test — MCP tools adapter with a real Validator, service mocked
No
DataSeederTest
Plain Mockito unit test — seeds only when the table is empty
No
SoftwareEngineerTest
Plain unit test — entity accessors and equals/hashCode
No
Spring skips Docker Compose startup in tests (spring.docker.compose.skip.in-tests
defaults to true), so the four @SpringBootTest classes need Postgres already running
or they fail to obtain a datasource:
docker compose up -d db && ./mvnw test
Troubleshooting
Connection to localhost:5332 refused — Postgres isn't running.
docker compose up -d db.
Port 5332 or 8080 already in use — find the holder with lsof -nP -iTCP:8080 -sTCP:LISTEN,
or run the app on another port: ./mvnw spring-boot:run -Dspring-boot.run.arguments=--server.port=8081.
The endpoint returns [] — DataSeeder inserts only when the row count is zero. On a
fresh database that means a seeding failure (check the startup log); on an existing volume
the table was emptied — the seeder will re-fill it on the next start.
Duplicate rows after restarts — the repository.count() > 0 guard in DataSeeder
prevents this: with ddl-auto=update the seeded rows persist in the postgres_data
volume, so the guard short-circuits the seeder on every start after the first.
IntelliJ: Could not find or load main class com.amigoscode.Application — the Maven
module isn't registered. Maven tool window → Reload All Maven Projects, then delete and
re-create the run configuration from the gutter arrow next to main().
nginx: host not found in upstream "app" (Option 3 only) — nginx started before the
app container was healthy. depends_on: condition: service_healthy normally prevents
this; if it still happens, docker compose --profile full up --build again.
Port 443 (or 80) already in use (Option 3 only) — find the holder with
lsof -nP -iTCP:443 -sTCP:LISTEN and stop it, or another process (e.g. a local
webserver) is bound to it.
nginx crash-loops on startup (Option 3 only) — usually means
./docker/nginx/generate-certs.sh was never run: Docker then bind-mounts an
empty certs/ directory, nginx finds no localhost.crt/localhost.key, and
fails to start. Run the script and bring the stack back up.
คัดลอกคอนฟิก stdio สำหรับ Cursor, Claude Desktop, Claude Code หรือ VS Code ได้ในหน้านี้ คอนฟิกนี้ใช้การติดตั้งของผู้เผยแพร่ ไม่ใช่พร็อกซีของ PlugMCP
Software Engineers Api ต้องใช้ API key ไหม?
ระเบียนนี้ไม่ได้ระบุตัวแปรสภาพแวดล้อมที่จำเป็น
Software Engineers Api ใช้ทรานสปอร์ตแบบใด?
Software Engineers Api ทำงานแบบโลคัลผ่าน stdio
Software Engineers Api ถูกตรวจสอบล่าสุดเมื่อใด?
ตรวจสอบล่าสุดเมื่อ 2026-10-28 Spring Boot 4 REST API with Spring Data JPA, backed by Postgres running in Docker Compose wrapped with Spring AI MCP Server layer