πŸ› οΈ 7 Mistakes I Made Writing My First Spring Boot APIs (And What I Do Differently Now)



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