Real-World Scenarios: When a “Working” API Isn’t Enough
Early in my career, I excitedly handed over a Spring Boot API to the Frontend team. At the time, I only focused on returning the correct JSON. The result was disastrous. Within a week, the system leaked over 1,000 user records because I exposed sensitive information in the response. Even worse, anyone with a link could delete someone else’s posts due to a lack of proper authorization.
Many Junior developers today are still repeating these mistakes. Code might run perfectly on a local machine but will quickly collapse under security audits or high user traffic. A real-world system demands much stricter encapsulation and security than a typical university project.
3 “Fatal Flaws” That Get Your API Rejected in Production
After refactoring a codebase of over 50,000 lines for a Fintech project, I identified the three most common mistakes:
- Lax Security Management: Many developers still use Basic Auth or store passwords in plain text. This is an open invitation for hackers.
- Entity Overexposure: Returning raw Database objects directly through the API is a severe error. It unintentionally exposes password hashes or unnecessary internal IDs.
- Hardcoded Configurations: Keeping JWT Secret Keys directly in the code means you have to rebuild the entire project just to rotate a key.
Architectural Choice: Why JWT?
When handling authentication, we usually choose between Sessions and Tokens. Traditional session-based auth is difficult to scale when you need to run Load Balancing across 3-4 different servers. Basic Auth is highly insecure because it sends credentials with every request.
JWT (JSON Web Token) has emerged as the gold standard for REST APIs. It allows the system to remain stateless, making it easy to integrate with both Web and Mobile Apps. Here is how I implement a standard project, combining PostgreSQL with multi-layered security.
Detailed Implementation Guide
1. Proper PostgreSQL Initialization and Configuration
First, visit Spring Initializr. You need to select these libraries: Spring Web, Spring Data JPA, PostgreSQL Driver, Spring Security, and Lombok.
Instead of using the outdated .properties file, I prefer application.yml for its clear hierarchical structure. Configure your DB connection as follows:
spring:
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:mydb}
username: ${DB_USER:postgres}
password: ${DB_PASS:password}
jpa:
hibernate:
ddl-auto: update
show-sql: false
Important Note: Never hardcode actual passwords in this file. The ${VAR:default} syntax allows you to flexibly pass environment variables when deploying with Docker or Kubernetes.
2. Setting up Models and Role-Based Access Control (RBAC)
A Role-based Access Control (RBAC) model is mandatory. You should separate User and Role into a Many-to-Many relationship. This approach allows you to easily promote a User to ADMIN without changing any code.
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
private Set<Role> roles = new HashSet<>();
}
3. Configuring Spring Security 6: The Toughest Challenge
Spring Security 6 has deprecated the old WebSecurityConfigurerAdapter. We have now moved toward functional configuration. This is where you define the “checkpoints” for your API.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
4. Managing the JWT Service
Use the jjwt library to generate tokens. A small tip: set a short expiration time, such as 15 minutes for the Access Token. Then, combine it with a Refresh Token to ensure a seamless yet secure user experience.
Docker Packaging: Production Ready
To avoid the “it works on my machine” syndrome, I always package applications into Docker. This ensures consistency between development and production environments.
A minimalist Dockerfile for the project:
FROM eclipse-temurin:17-jdk-alpine
WORKDIR /app
COPY target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Combined with docker-compose.yml, you can launch both the App and the Database with a single command. Infrastructure management becomes much more manageable.
A Final Word from Experience
I once spent an entire night fixing a bug caused by a single faulty XML config file in a legacy system. The biggest lesson is: Write code for humans to read, not just for machines to execute. Adopting Spring Boot 3 with transparent configurations saves the maintenance team dozens of hours every month.
Don’t forget to write Integration Tests for Login and Register flows. A solid test suite gives you the confidence to hit the Deploy button on a Friday afternoon without worrying about getting a call from your boss at midnight. Once you master these steps, you are truly ready to build professional Backend systems.

