Why I built it
I wanted a backend project that went beyond basic CRUD operations. Since I was also building a personal portfolio and blog, a content-management API gave me a practical problem to solve while learning how the different parts of a backend system work together.
The goal was not to create an overly complicated platform. I wanted to build a maintainable API that could manage posts, categories, and comments while supporting public content access and protected administrative operations.
Designing the application structure
I organized the application using a controller-service-repository structure. Controllers handle HTTP requests, services contain application logic, and repositories communicate with the database.
Separating these responsibilities made the code easier to navigate and test. It also helped prevent request-handling concerns from becoming mixed with database access and business rules.
- Controllers define the API endpoints and validate incoming requests.
- Services coordinate application rules and data transformations.
- Repositories use Spring Data JPA to access PostgreSQL.
- DTOs control the data accepted and returned by the API.
Protecting administrative operations
The API allows public users to read published content, while operations such as creating, editing, and deleting content require authentication.
I implemented JWT-based authentication with Spring Security. After a successful login, the client receives a token that is included in later requests. A security filter validates that token before protected endpoints can be accessed.
This part of the project helped me better understand that authentication is not only about implementing a login endpoint. It also involves password storage, request filtering, authorization rules, token validation, and consistent error responses.
Treating the database schema as code
PostgreSQL stores the application data, while Flyway manages changes to the database schema. Instead of manually modifying the production database, each schema change is represented by a versioned migration file.
Using migrations made the state of the database reproducible. A new environment can apply the same migration history and arrive at the schema expected by the application.
Adding pagination and filtering
Content collections can grow over time, so returning every record in one response would not scale well. I added pagination to limit the number of records returned by each request.
The API also supports filtering and sorting so clients can request a more useful subset of the available content. Building this feature made me think more carefully about query parameters, default values, response metadata, and predictable API behaviour.
Testing behaviour, not only implementation
I used JUnit 5 to verify important application behaviour. The tests helped me check service logic, expected responses, and failure cases without repeatedly exercising every endpoint by hand.
I am still improving the project’s test coverage, but introducing automated tests changed how I approach implementation. Code becomes easier to test when responsibilities are separated and dependencies are kept explicit.
Learning from deployment
I containerized the application with Docker and previously deployed it using Railway with a Neon PostgreSQL database. The live instance is currently offline, but the deployment configuration remains available in the repository.
Deployment exposed concerns that are easy to overlook during local development, including environment variables, database connectivity, migration order, production configuration, and application startup failures.
What I learned
The most valuable result of this project was not a single framework feature. It was seeing how architecture, persistence, authentication, validation, testing, and deployment influence one another.
There are still areas I want to improve, including broader integration testing, observability, token-management decisions, and performance measurement. For me, the project is a foundation rather than a finished demonstration of everything involved in backend engineering.
- Clear boundaries make applications easier to understand and change.
- Database migrations are part of the application, not an afterthought.
- Security decisions affect the entire request lifecycle.
- Deployment is one of the best ways to uncover incorrect assumptions.
- A working project can still have many meaningful areas for improvement.