Summary written by Claude!
A Spring Boot REST API that fetches Pokémon data from the PokéAPI and lets you manage a personal favorites collection stored in MongoDB.
Built as a recap project covering the core building blocks of a modern Spring Boot backend: an outbound HTTP client, layered architecture with DTO separation, bean validation, centralized exception handling, and full CRUD persistence.
| Area | Technology |
|---|---|
| Language | Java 25 |
| Framework | Spring Boot 4.0.x (Spring Framework 7) |
| HTTP client | RestClient |
| Persistence | Spring Data MongoDB |
| Validation | Jakarta Bean Validation |
| JSON | Jackson 3 |
| Boilerplate | Lombok |
| Testing | JUnit 5, Mockito, MockMvc, MockRestServiceServer |
| Build | Maven |
The app follows a clean, layered flow. Each layer talks only to the one below it:
Client ──▶ Controller ──▶ Service ──▶ Repository ──▶ MongoDB
│
└──▶ RestClient ──▶ PokéAPI
- Controller – HTTP layer: routing, request/response (de)serialization, validation.
- Service – business logic: calls the PokéAPI, maps data, and persists favorites.
- Repository – Spring Data MongoDB; CRUD methods are generated automatically.
Two distinct DTO families keep external and internal concerns apart:
- PokéAPI DTOs (
ExternalPokemonDtoand its nested records) mirror the raw PokéAPI JSON and are used only for mapping the external response. - Application DTOs (
FavoriteRequest,NicknameUpdateRequest,PokemonResponse) define exactly what each endpoint accepts and returns — never exposing the external shape or the database entity directly.
| Method | Path | Description |
|---|---|---|
GET |
/api/pokemon/{name} |
Fetch simplified Pokémon data from the PokéAPI |
GET |
/api/pokemon/random |
Fetch a random Pokémon |
GET |
/api/pokemon/type/{type} |
List all Pokémon of a given type |
GET |
/api/pokemon/compare/{name1}/{name2} |
Compare two Pokémon by height and weight |
Example — GET /api/pokemon/pikachu
{
"pokemonId": 25,
"pokemonName": "pikachu",
"pictureUrl": "https://raw.githubusercontent.com/PokeAPI/sprites/.../25.png",
"height": 4,
"weight": 60,
"types": ["electric"]
}Random — GET /api/pokemon/random picks a random id and returns the same shape as above. Because /random is a literal path it always takes priority over /{name}, so it is never mistaken for a Pokémon called "random".
By type — GET /api/pokemon/type/fire
Fetches all Pokémon of a type from the PokéAPI's /type/{type} endpoint and returns a simplified list (a single API call — no per-Pokémon fan-out). The id is parsed from each entry's resource URL.
[
{ "pokemonId": 4, "pokemonName": "charmander" },
{ "pokemonId": 5, "pokemonName": "charmeleon" },
{ "pokemonId": 6, "pokemonName": "charizard" }
]Compare — GET /api/pokemon/compare/charizard/blastoise
Fetches both Pokémon and merges the results, reporting which is heavier/taller and by how much. The difference is absolute, so the argument order doesn't change the magnitude.
{
"heavierPokemon": "charizard",
"weightDifference": 50,
"tallerPokemon": "charizard",
"heightDifference": 1
}| Method | Path | Description |
|---|---|---|
POST |
/api/collection |
Add a Pokémon to favorites |
GET |
/api/collection |
List all favorites |
GET |
/api/collection/{id} |
Get a single favorite by id |
GET |
/api/collection/search?name=... |
Search favorites by Pokémon name or nickname |
PUT |
/api/collection/{id} |
Update a favorite's nickname |
DELETE |
/api/collection/{id} |
Remove a favorite |
Create — POST /api/collection
{
"pokemonName": "pikachu",
"nickname": "My Starter"
}The service verifies the Pokémon exists via the PokéAPI, copies the relevant fields, assigns a generated UUID, and stores the result:
{
"id": "4b903fd9-93df-4fd9-a4fc-1b89b7d1b0f5",
"pokemonId": "25",
"nickname": "My Starter",
"pokemonName": "pikachu",
"pictureUrl": "https://raw.githubusercontent.com/...",
"height": 4,
"weight": 60,
"types": ["electric"]
}Update — PUT /api/collection/{id} (only the nickname changes; Pokémon data stays intact)
{ "nickname": "Arena Champion" }Search — GET /api/collection/search?name=pika
Matches the term against both pokemonName and nickname, case-insensitive and as a substring (so pika finds pikachu). Returns a list; an empty array if nothing matches.
[
{
"id": "4b903fd9-93df-4fd9-a4fc-1b89b7d1b0f5",
"pokemonId": "25",
"nickname": "My Starter",
"pokemonName": "pikachu",
"pictureUrl": "https://raw.githubusercontent.com/...",
"height": 4,
"weight": 60,
"types": ["electric"]
}
]Implemented with a Spring Data derived query (findByPokemonNameContainingIgnoreCaseOrNicknameContainingIgnoreCase) — the method name itself defines the query, so no manual MongoDB code is needed.
A global @RestControllerAdvice translates exceptions into consistent JSON responses:
| Exception | HTTP status | When |
|---|---|---|
PokemonNotFoundException |
404 |
The PokéAPI has no such Pokémon |
CollectionEntryNotFoundException |
404 |
No favorite with the requested id |
MethodArgumentNotValidException |
400 |
Request body fails validation |
All errors share the same body:
{
"message": "Pokemon not found: missingno",
"timestamp": "2026-06-01T10:15:00"
}- JDK 25
- A MongoDB instance (e.g. a free MongoDB Atlas cluster)
The connection string is read from an environment variable, so no secrets live in the repo:
# src/main/resources/application.properties
spring.mongodb.uri=${MONGO_DB_URI}Set MONGO_DB_URI before running, e.g.:
export MONGO_DB_URI="mongodb+srv://<user>:<password>@<cluster>.mongodb.net/pokedex?retryWrites=true&w=majority"Include the database name (/pokedex above) in the URI, otherwise MongoDB defaults to test.
./mvnw spring-boot:runThe API is then available at http://localhost:8080.
./mvnw testThe suite includes unit tests for the service layer (repository mocked with Mockito, the PokéAPI faked with MockRestServiceServer — no real network or database) and web-layer integration tests for the controllers (@WebMvcTest + MockMvc, covering routing, validation, and exception mapping).
src/main/java/org/example/pokemonapi_recapproject
├── configuration # RestClient bean
├── controller # PokeController, CollectionController
├── dto # external PokéAPI records + application DTOs
├── exception # custom exceptions + global handler + ErrorResponse
├── model # Favorite (@Document)
├── repository # FavoriteRepository (MongoRepository)
└── service # PokeService
- Consuming an external REST API with Spring's
RestClientand per-call error translation viaonStatus - Mapping deeply nested external JSON into clean DTOs (including
@JsonPropertyfor non-Java-friendly keys) - Constructor-based dependency injection and a strict controller → service → repository layering
- Bean Validation on request bodies
- Centralized exception handling with
@RestControllerAdvice - Full CRUD persistence with Spring Data MongoDB