A RESTful CRUD API for managing Products and their nested Items, built with Java 17 and Spring Boot. Produced for the Zest India IT Services Java Backend Developer hiring assignment.
| Concern | Choice |
|---|---|
| Language / Runtime | Java 17 |
| Framework | Spring Boot 3.3 |
| Persistence | Spring Data JPA (Hibernate) |
| Database | PostgreSQL (MySQL also supported), H2 for tests |
| Security | Spring Security, JWT access tokens + rotating refresh tokens |
| API docs | springdoc-openapi (Swagger UI) |
| Testing | JUnit 5, Mockito, Spring Boot Test, H2 |
| Containerization | Docker, Docker Compose |
| Build | Maven |
The codebase follows a classic layered / clean architecture, one package per concern:
com.zestindia.productapi
├── controller REST endpoints (thin: validation + delegation only)
├── service Business logic, interfaces + impl subpackage
├── repository Spring Data JPA repositories
├── entity JPA entities (Product, Item, User, RefreshToken)
├── dto Request/response DTOs, kept separate from entities
├── mapper MapStruct entity <-> DTO mappers
├── security JWT utilities, filter, UserDetailsService
├── config Security, OpenAPI, async executor, dev data seeder
└── exception Custom exceptions + centralized @RestControllerAdvice
Why this shape: controllers never touch repositories directly, entities never leak past the service layer (DTOs only cross the controller boundary), and cross-cutting concerns (error handling, security, async execution) live in their own packages so they can be reasoned about independently of business logic.
product (id, product_name, created_by, created_on, modified_by, modified_on)
item (id, product_id -> product.id, quantity)
Product 1---N Item, matching the schema given in the assignment. Indexes are defined on
product.product_name (search) and item.product_id (the FK join path used by the nested
/items endpoint).
- Versioned, resource-oriented routes under
/api/v1/. Itemis modeled as a sub-resource ofProduct:/api/v1/products/{id}/items.- Collection endpoints (
GET /products,GET /products/{id}/items) are paginated via Spring's standard?page=&size=&sort=query params and return a consistentPageResponse<T>envelope. - All errors return a consistent JSON shape (
ErrorResponse): timestamp, status, error, message, path, and optional field-leveldetailsfor validation failures.
- Stateless JWT authentication.
POST /api/v1/auth/loginand/registerreturn a short-lived access token (15 min default) and an opaque, DB-backed refresh token (7 days default). - Refresh tokens are rotated on every use (
POST /api/v1/auth/refresh): the old token is deleted and a new pair issued, so a stolen refresh token that gets replayed after the legitimate client refreshes will fail (single active refresh token per user). - Role-based authorization:
ROLE_USERcan create/update products; onlyROLE_ADMINcan delete. Reads are public. A defaultadmin/Admin@123user is seeded on startup outside thetestprofile — change this immediately in any real deployment. - Passwords are hashed with BCrypt. CORS is configured centrally; HTTPS termination is expected to happen at the load balancer / ingress in production (not handled in-process).
Security/audit events (register, login, refresh) are logged through AsyncAuditService, annotated
with @Async, so they never add latency to the request path — a small, honest example of "async
processing where applicable" rather than forcing async onto the core CRUD path where it isn't
warranted.
docker compose up --buildThis starts PostgreSQL and the API together. The API will be available at
http://localhost:8080, Swagger UI at http://localhost:8080/swagger-ui.html.
-
Start a PostgreSQL instance and create a
productdbdatabase (or pointDB_URLat MySQL). -
Copy
.env.exampleto.envand adjust credentials if needed, or export the same variables. -
Build and run:
mvn clean package java -jar target/product-api-1.0.0.jar
or, for iterative development:
mvn spring-boot:run
# Register
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"Password123"}'
# Login
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"Password123"}'
# Use the returned accessToken as a Bearer token
curl -X POST http://localhost:8080/api/v1/products \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json" \
-d '{"productName":"Wireless Mouse"}'
# Refresh (rotates the refresh token)
curl -X POST http://localhost:8080/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"<refreshToken>"}'| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/register |
Public | Create a user account |
| POST | /api/v1/auth/login |
Public | Obtain access + refresh tokens |
| POST | /api/v1/auth/refresh |
Public | Rotate refresh token |
| POST | /api/v1/auth/logout |
Public | Revoke a refresh token |
| GET | /api/v1/products |
Public | Paginated product list, ?name= filter |
| GET | /api/v1/products/{id} |
Public | Get one product |
| POST | /api/v1/products |
USER or ADMIN | Create a product |
| PUT | /api/v1/products/{id} |
USER or ADMIN | Update a product |
| DELETE | /api/v1/products/{id} |
ADMIN only | Delete a product |
| GET | /api/v1/products/{id}/items |
Public | Paginated items for a product |
| POST | /api/v1/products/{id}/items |
USER or ADMIN | Add an item to a product |
Full interactive documentation: GET /swagger-ui.html (OpenAPI JSON at /v3/api-docs).
mvn test- Unit tests (
servicepackage):ProductServiceImplTest,ItemServiceImplTest— mock the repository/mapper layers with Mockito and assert business logic and exception paths in isolation. - Controller slice tests (
controllerpackage):ProductControllerTest—@WebMvcTest+MockMvc, verifying HTTP status codes, validation, and role-based access without a real DB. - Integration tests (
integrationpackage):ProductIntegrationTest— full@SpringBootTestagainst an in-memory H2 database (testprofile), exercising registration, login, the full product/item CRUD lifecycle, pagination, and refresh-token rotation end to end through the real Spring Security filter chain.
All configuration is environment-variable driven (see application.yml / .env.example), so the
same image runs unmodified across dev, docker-compose, and production — only the environment
changes.
- Both PostgreSQL and MySQL drivers are bundled; the active one is selected via
DB_DRIVER/DB_URL/JPA_DIALECT. docker-compose.yml wires up PostgreSQL by default. ddl-auto: updateis used for convenience in this assignment; a production setup would use a migration tool (Flyway/Liquibase) instead.- The submission repository does not commit real secrets —
JWT_SECRETinapplication.ymlhas a placeholder default and should be overridden via environment variable in any shared environment.