Registration, errors and dev data
A registration API with validation, one consistent error format, and demo credentials that only exist in the dev profile.
You will build
a registration endpoint, a centralised error handler, and a dev-profile initializer for the demo user and client
You will understand
why demo credentials must never be a migration, and how to scope CSRF exemptions instead of disabling them everywhere
- one controller, one service, one error handler
A real template needs a clean way to create users and a clean failure contract for API consumers.
It also needs to stop pretending development credentials belong in production configuration.
This part adds all three.
Registration request
Use a DTO instead of binding directly to UserEntity:
public record RegisterUserRequest(
@NotBlank
@Size(min = 3, max = 50)
@Pattern(regexp = "^[a-zA-Z0-9._-]+$")
String username,
@NotBlank
@Email
@Size(max = 255)
String email,
@NotBlank
@Size(min = 8, max = 72)
String password
) {}
Why cap a password at 72 here? If your configured password algorithm has implementation-specific input behavior, document and validate the boundary intentionally rather than accepting arbitrary payloads. Match the validation to the encoder you actually use.
Response:
public record UserResponse(
UUID id,
String username,
String email
) {}
No password hash ever leaves the service boundary.
Registration service
Core steps:
normalize username/email
↓
check duplicates
↓
load USER role
↓
encode password
↓
initialize account state
↓
save
Example:
String username = request.username().trim();
String email = request.email().trim().toLowerCase(Locale.ROOT);
Reject duplicate username/email with a conflict exception.
Then:
user.setPassword(
passwordEncoder.encode(request.password())
);
user.setEnabled(true);
user.setAccountNonExpired(true);
user.setAccountNonLocked(true);
user.setCredentialsNonExpired(true);
user.getRoles().add(userRole);
Registration controller
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
UserResponse register(
@Valid @RequestBody RegisterUserRequest request
) {
return registrationService.register(request);
}
}
Security rule for registration
Public registration must be explicitly permitted:
.requestMatchers(
HttpMethod.POST,
"/api/v1/auth/register"
)
.permitAll()
If your normal application chain uses CSRF protection for browser flows, ignore CSRF only for the intended stateless registration endpoint rather than disabling it everywhere.
That distinction becomes more important as the application grows.
Centralized API error response
Create a stable error structure:
public record ApiErrorResponse(
Instant timestamp,
int status,
String error,
String message,
String path,
Map<String, String> validationErrors
) {}
Then handle exceptions in @RestControllerAdvice.
Examples:
ResourceConflictException → 409
ResourceNotFoundException → 404
InvalidRequestException → 400
validation failure → 400
unexpected error → 500 generic message
Do not return arbitrary internal exception messages from the generic Exception handler.
A safe generic response is:
An unexpected error occurred
while the server logs preserve the internal diagnostic information.
Validation errors
For MethodArgumentNotValidException, build a deterministic map:
{
"message": "Validation failed",
"validationErrors": {
"email": "must be a well-formed email address",
"password": "size must be between 8 and 72"
}
}
This is much more useful to API clients than returning a stack trace or framework-specific exception payload.
Development seed data belongs in dev
Create an initializer:
@Component
@Profile("dev")
public class DevelopmentDataInitializer
implements ApplicationRunner {
// ...
}
The profile annotation is the security boundary:
flowchart TD
Start[Application starts]
P{Active profile}
P -->|dev| Seed[Create demo user/client]
P -->|prod| NoSeed[No demo credentials]
P -->|test| Fixtures[Test setup]
The initializer should:
- load the
USERandADMINroles seeded by Flyway; - create/update the development user;
- ensure the dev user has both roles;
- create
demo-clientonly when it does not already exist.
Development user:
username: user
email: user@example.com
password: password
roles: USER, ADMIN
Development client:
client_id: demo-client
client_secret: demo-secret
Those values are intentionally obvious because they are development fixtures. The real safety mechanism is that they do not run outside dev.
Start development explicitly
SPRING_PROFILES_ACTIVE=dev ./mvnw spring-boot:run
This is better than quietly setting dev as the application’s universal default.
Test registration
curl -i \
-X POST http://localhost:9000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "alice",
"email": "alice@example.com",
"password": "Password123!"
}'
Expected:
HTTP/1.1 201
Duplicate username should return:
409 Conflict
Invalid email should return:
400 Bad Request
with validation information.
Why demo data should not be a Flyway migration
Essential domain constants such as baseline roles can reasonably be migration data.
Demo credentials are different. If user/password and demo-client/demo-secret are inserted unconditionally through schema migrations, they can accidentally appear in production.
Use:
Flyway → required domain/schema baseline
@Profile("dev") initializer → disposable development fixtures
Figure: startup responsibilities
flowchart LR
Flyway[Flyway migrations] --> Schema[Schema + required roles]
Dev["@Profile dev initializer"] --> Demo[Demo user + demo OAuth client]
Schema --> App[Application]
Demo --> App
The code
This part corresponds to these commits in the repository:
21e8771feat: add user registrationf28b64dfeat: add centralized API exception handling56affa2refactor: move demo OAuth client to dev seed data
Check out the last one to see the project exactly as this part leaves it:
git checkout 56affa2
References
- Spring Boot validation: https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.html
- Spring Boot profiles: https://docs.spring.io/spring-boot/reference/features/profiles.html
- Spring Security CSRF: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html
Next: turn our application into a manageable authorization server with ADMIN-protected OAuth client and scope APIs.