REST API Design: DTOs, Exception Handling, and CORS
Three unglamorous decisions that separate a REST API a frontend team can actually use from one they'll keep filing bugs against: what you return, how you fail, and who's allowed to call you.
Getting a REST endpoint to return 200 OK is the easy part. What actually determines whether an API is pleasant to build a frontend against is what shape it returns, what it does when something goes wrong, and whether the browser is even allowed to call it.
DTOs exist to hide fields, not just rename them
class User {
private int id;
private String username;
private String email;
private String password; // never leaves the server
}
class UserDto {
private String username;
private String email;
}The entity has a password field because the database needs it. The DTO doesn't, because the client never should see it. Manual field-by-field copying works but doesn't scale past a couple of DTOs โ that's what MapStruct and ModelMapper exist to automate.
MapStruct vs ModelMapper
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "email", target = "emailAddress")
@Mapping(source = "name", target = "fullName")
UserDTO toUserDTO(UserApp userApp);
}modelMapper.typeMap(UserApp.class, UserDTO.class).addMappings(mapper -> {
mapper.map(UserApp::getEmail, UserDTO::setEmailAddress);
mapper.map(UserApp::getName, UserDTO::setFullName);
});
UserDTO userDTO = modelMapper.map(user, UserDTO.class);Info
MapStruct generates real mapping code at compile time โ a mismatched field name is a compile error, and there's no reflection cost at runtime. ModelMapper resolves mappings via reflection at runtime โ faster to wire up for a quick prototype, but a renamed field fails silently until you notice a null in production.
Exception handling with @ControllerAdvice + ProblemDetail
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(String message) { super(message); }
}
@ControllerAdvice
public class CustomizedExceptionHandling {
@ExceptionHandler(StudentNotFoundException.class)
ProblemDetail handleExceptions(StudentNotFoundException exception, WebRequest webRequest) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, exception.getMessage());
problemDetail.setType(URI.create("http://localhost:8080/errors/not-found"));
problemDetail.setTitle("Not Found!");
return problemDetail;
}
}One handler, every controller
@ControllerAdvice centralizes error formatting so every endpoint returns the same error shape instead of each controller inventing its own. ProblemDetail is Spring's built-in implementation of RFC 7807 (application/problem+json) โ a standard error format a frontend can parse generically instead of guessing your API's custom error schema.
CORS: an allowlist, not a formality
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Collections.singletonList("*"));
config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}Warning
setAllowedOriginPatterns("*") combined with setAllowCredentials(true) is convenient in a course demo and dangerous in production โ it lets any origin send credentialed requests. In a real deployment, list your actual frontend origins explicitly instead of wildcarding them.
JWT Authentication: Anatomy of a Filter Chain
NextDatabase Normalization: 1NF to 3NF, With the Anomaly Each Step Fixes
Related articles
MyBatis Dynamic SQL: The Tags That Actually Matter
if, where, set, foreach, and choose cover almost every real query I write. A field guide with the patterns I reach for most.
Thymeleaf for Spring Boot: From th:text to Fragments
A practical path through Thymeleaf โ basic attribute binding, object selection, and the fragment pattern that kills copy-pasted navbars.
Inversion of Control & Dependency Injection: The Bean I Kept Recreating
Six classes, one Person object, one `new Person(...)` copy-pasted six times. That's the problem IoC actually solves โ not an abstract principle, a concrete duplication bug.