This content originally appeared on DEV Community and was authored by Ashish Saran Shakya
Spring Boot makes building APIs feel deceptively easy.
When I started out, I focused on just βgetting things working.β But when things breakβor just scale badlyβyou realize that working code isnβt always good code.
Here are 7 mistakes I made writing my first Spring Boot APIs, and what I now do to write cleaner, more maintainable backends.
1. Not Using Validation Annotations (@Valid
, @NotNull
, etc.)
What I did:
Manually validated request payloads deep inside controllers or services. Missed fields caused confusing errors or null pointer exceptions.
if (userRequest.getEmail() == null) {
throw new IllegalArgumentException("Email is required");
}
Why it’s a problem:
- Makes code noisy and repetitive
- Delays validation too deep into the flow
- Prone to human error
What I do now:
Use Spring’s built-in validation support with annotations and @Valid
.
public class UserRequest {
@NotNull
@Email
private String email;
}
And in the controller:
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody UserRequest request) {
...
}
Pair this with @ControllerAdvice
to handle validation exceptions cleanly.
2. Hardcoding config with @Value
instead of using @ConfigurationProperties
What I did:
Injected config values everywhere using @Value
.
@Value("${aws.s3.bucket}")
private String bucketName;
Why it’s a problem:
- Scattered config makes things hard to manage
- No way to validate values
- Harder to write unit tests
What I do now:
Use @ConfigurationProperties
to bind entire config blocks into structured classes.
@ConfigurationProperties(prefix = "aws.s3")
public class S3Properties {
private String bucket;
// getters/setters
}
This gives you validation, IDE support, and cleaner wiring.
3. Logging the Wrong Way
What I did:
Used System.out.println()
and vague messages like βError occurredβ or βDebug hereβ.
Why it’s a problem:
- Not visible in production logs
- Lacks context (who, what, when)
- No severity levels or structured format
What I do now:
Use SLF4J with clear, parameterized logging:
log.info("User {} uploaded file {}", userId, fileName);
And for errors:
log.error("Failed to process request for user {}", userId, e);
Be intentional with log levels: info
, warn
, error
, and avoid logging entire stack traces unless necessary.
4. Messy Exception Handling
What I did:
Wrapped everything in try-catch blocks, often rethrowing or printing stack traces manually.
try {
...
} catch (Exception e) {
e.printStackTrace();
throw e;
}
Why it’s a problem:
- Duplicates logic
- Makes error responses inconsistent
- Hides real issues in production
What I do now:
Define custom exceptions and handle them centrally using @ControllerAdvice
.
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String msg) {
super(msg);
}
}
And in a global exception handler:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
}
Clean, consistent, and maintainable.
5. Overusing @Autowired
Field Injection
What I did:
Injected everything using @Autowired
on fields.
@Autowired
private UserService userService;
Why it’s a problem:
- Makes dependencies hard to track
- Breaks immutability
- Difficult to write unit tests
What I do now:
Use constructor injection:
@Service
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
Bonus: With one constructor, Spring injects it automaticallyβno @Autowired
needed.
6. Forgetting API Documentation (like Swagger/OpenAPI)
What I did:
Left API documentation in someone’s Notion page or none at all.
Why it’s a problem:
- Frontend teams have to guess request/response formats
- No way to test endpoints quickly
- Becomes a bottleneck during integration
What I do now:
Use springdoc-openapi to generate live docs with minimal setup:
<!-- build.gradle / pom.xml -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.14</version>
</dependency>
Then access Swagger UI at:
http://localhost:8080/swagger-ui/index.html
It reflects your controller annotations and helps both devs and testers.
7. Mixing DTOs with Entities
What I did:
Used JPA entities directly in request bodies and responses.
Why it’s a problem:
- Leaks internal persistence logic
- Adds accidental complexity (lazy loading, cascade issues)
- Makes refactoring risky
What I do now:
Create dedicated DTO classes:
public class UserDto {
private String name;
private String email;
}
Use mappers like MapStruct, ModelMapper, or just write a utility:
public UserDto toDto(User user) {
return new UserDto(user.getName(), user.getEmail());
}
Entities stay clean. APIs stay stable.
Final Thoughts
Spring Boot abstracts a lot of complexityβbut with that power comes responsibility.
These 7 mistakes cost me time, performance, and headaches. But fixing them made me a better engineer.
Whatβs one mistake you made in your early Spring Boot journey?
Would love to hear your thoughts below
This content originally appeared on DEV Community and was authored by Ashish Saran Shakya