Software Engineers API
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
Prerequisites
- JDK 21
- Docker with Compose v2 (
docker compose, notdocker-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.
Then:
curl -s localhost:8080/api/v1/software-engineers | jq
[
{ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "James", "techStack": ["js, node, vue, java, spring boot"] },
{ "id": "8d3e1b2a-9c4f-4a17-bf90-1d2e3f4a5b6c", "name": "Jamila", "techStack": ["js, node, vue, python, FastApi"] }
]
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:
curl -sk https://localhost/api/v1/software-engineers | jq
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
requests.http in the project root contains this call, runnable directly from
IntelliJ's HTTP client.
SoftwareEngineer
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".
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):
{
"software-engineers": { "url": "http://localhost:8080/mcp", "type": "http" }
}
Raw requests — Streamable-HTTP is a two-step handshake. Call initialize
first and read the mcp-session-id response header:
curl -i -X POST http://localhost:8080/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
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
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.
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
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.
License
Licensed under the Apache License, Version 2.0.
Copyright 2026 Omar Nizam and SyncMind Digital.