Feature 5: API Contract & Documentation
Purpose: This guide provides step-by-step instructions for establishing API contracts and generating comprehensive documentation using OpenAPI/Swagger.
Dependencies:
- Requires SpringDoc OpenAPI dependency (already in Springular)
- Optional: OpenAPI Generator (Feature 4) for client generation
- Recommended: Feature 9 (Test Coverage) for contract testing
Related Documents:
- Boilerplate Enhancements Solution Architecture - System design overview
- Boilerplate Enhancements Epic - Capability definition
Overviewโ
What This Guide Coversโ
API Contract & Documentation establishes API contracts and generates documentation:
- OpenAPI 3.0 specifications for all REST endpoints
- SpringDoc @Operation and @ApiResponse annotations on controllers
- Generated Swagger UI documentation at /swagger-ui
- Request/response examples and schema validation
- API versioning strategy
- Optional: API contract testing with Spring Cloud Contract or Pact
What's Included:
- SpringDoc configuration
- Controller annotations
- Swagger UI setup
- API versioning
- Request/response schemas
- Optional: Contract testing setup
What's NOT Included:
- โ GraphQL API documentation (different concern)
- โ WebSocket API documentation (different concern)
- โ Internal/private API documentation (could be added)
Prerequisitesโ
- Springular boilerplate setup
- Understanding of OpenAPI 3.0 specification
- Understanding of Spring REST controllers
- SpringDoc OpenAPI dependency (already included)
Current Stateโ
Codebase Exploration Findings:
- โ SpringDoc OpenAPI v2.6.0 dependency included
- โ
Swagger UI endpoints configured:
/swagger-ui/**,/v3/api-docs/** - โ NO OpenAPI specification files
- โ NO @Operation annotations on controllers
- โ NO @ApiResponse annotations
- โ NO API versioning strategy
Current APIs (need documentation):
- Authentication:
/auth/login,/auth/register,/auth/refresh - Users:
/users/me,/users/forgot-password,/users/reset-password - Payments:
/payments/subscription,/payments/invoices/list,/payments/checkout/create-checkout-session - OAuth2:
/oauth2/authorize/{provider},/oauth2/callback/*
Implementation Stepsโ
Step 1: Configure SpringDoc OpenAPIโ
File: server/src/main/java/com/saas/springular/common/config/OpenApiConfiguration.java (NEW)
Purpose: Configure SpringDoc for API documentation generation.
Create configuration class:
package com.saas.springular.common.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class OpenApiConfiguration {
@Value("${server.port:8081}")
private int serverPort;
@Bean
public OpenAPI springularOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Springular API")
.description("REST API for Springular SaaS application")
.version("v1.0")
.contact(new Contact()
.name("Springular Team")
.email("support@springular.com")
.url("https://springular.com"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")))
.servers(List.of(
new Server()
.url("http://localhost:" + serverPort)
.description("Local development server"),
new Server()
.url("https://api.springular.com")
.description("Production server")))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.name("Bearer Authentication")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Enter JWT token obtained from /auth/login")));
}
}
Configuration Explanation:
- Defines API metadata (title, description, version)
- Adds contact information
- Configures servers (local and production)
- Adds JWT bearer authentication security scheme
- All controllers will inherit this configuration
Step 2: Configure SpringDoc Propertiesโ
File: server/src/main/resources/application.yml
Purpose: Configure SpringDoc behavior and Swagger UI.
Add springdoc configuration:
springdoc:
api-docs:
path: /v3/api-docs
enabled: true
swagger-ui:
path: /swagger-ui
enabled: true
operations-sorter: method
tags-sorter: alpha
display-request-duration: true
default-models-expand-depth: 3
default-model-expand-depth: 3
show-actuator: false
group-configs:
- group: public-api
paths-to-match:
- /api/**
packages-to-scan:
- com.saas.springular
Configuration Explanation:
api-docs.path: /v3/api-docs- OpenAPI JSON endpointswagger-ui.path: /swagger-ui- Swagger UI pathoperations-sorter: method- Sort by HTTP methodtags-sorter: alpha- Sort tags alphabeticallydisplay-request-duration: true- Show request duration in UIshow-actuator: false- Hide actuator endpoints from docsgroup-configs- Group APIs by path prefix
Step 3: Add Annotations to Authentication Controllerโ
File: server/src/main/java/com/saas/springular/user/api/AuthenticationController.java
Purpose: Document authentication endpoints with OpenAPI annotations.
Add annotations:
package com.saas.springular.user.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/auth")
@Tag(name = "Authentication", description = "Authentication and user registration endpoints")
public class AuthenticationController {
private final AuthenticationService authenticationService;
public AuthenticationController(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}
@PostMapping("/login")
@Operation(
summary = "User login",
description = "Authenticate user with email and password, returns JWT access and refresh tokens"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Successfully authenticated",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = JwtResponse.class)
)
),
@ApiResponse(
responseCode = "401",
description = "Invalid credentials",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)
)
),
@ApiResponse(
responseCode = "422",
description = "Validation error",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)
)
)
})
public ResponseEntity<JwtResponse> login(
@Valid @RequestBody
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User login credentials",
required = true,
content = @Content(
schema = @Schema(implementation = LoginRequest.class)
)
)
LoginRequest loginRequest
) {
JwtResponse response = authenticationService.loginUser(
loginRequest.getEmail(),
loginRequest.getPassword()
);
return ResponseEntity.ok(response);
}
@PostMapping("/register")
@Operation(
summary = "User registration",
description = "Register a new user account with email and password"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "User successfully registered"
),
@ApiResponse(
responseCode = "400",
description = "Email already in use",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)
)
),
@ApiResponse(
responseCode = "422",
description = "Validation error",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)
)
)
})
public ResponseEntity<Void> register(
@Valid @RequestBody
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "User registration details",
required = true,
content = @Content(
schema = @Schema(implementation = SignupRequest.class)
)
)
SignupRequest signupRequest
) {
authenticationService.registerUser(signupRequest);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PostMapping("/refresh")
@Operation(
summary = "Refresh access token",
description = "Generate new access token using refresh token"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "New access token generated",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = JwtResponse.class)
)
),
@ApiResponse(
responseCode = "401",
description = "Invalid or expired refresh token",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)
)
)
})
public ResponseEntity<JwtResponse> refreshToken(
@Valid @RequestBody
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Refresh token request",
required = true,
content = @Content(
schema = @Schema(implementation = RefreshTokenRequest.class)
)
)
RefreshTokenRequest request
) {
JwtResponse response = authenticationService.refreshToken(request.getRefreshToken());
return ResponseEntity.ok(response);
}
}
Key Annotations:
@Tag- Groups endpoints in Swagger UI@Operation- Describes endpoint (summary, description)@ApiResponses- Documents all possible responses@ApiResponse- Individual response documentation@Content- Response body content type and schema@Schema- Links to DTO class@RequestBody- Documents request body
Step 4: Add Schema Annotations to DTOsโ
File: server/src/main/java/com/saas/springular/user/api/LoginRequest.java
Purpose: Document DTO schemas with examples.
Add annotations:
package com.saas.springular.user.api;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Schema(description = "User login credentials")
public class LoginRequest {
@Schema(
description = "User email address",
example = "user@example.com",
required = true
)
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@Schema(
description = "User password",
example = "SecurePass123!@#",
required = true,
minLength = 8
)
@NotBlank(message = "Password is required")
private String password;
// Getters and setters
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Similarly for JwtResponse.java:
@Schema(description = "JWT authentication response")
public class JwtResponse {
@Schema(
description = "JWT access token",
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
)
private String accessToken;
@Schema(
description = "JWT refresh token",
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
)
private String refreshToken;
@Schema(
description = "Token type",
example = "Bearer"
)
private String tokenType = "Bearer";
// Getters and setters
}
Key Annotations:
@Schemaon class - Describes the DTO@Schemaon fields - Describes each field with exampleexample- Shown in Swagger UI "Try it out"required- Indicates required fieldsminLength,maxLength- Validation constraints
Step 5: Create Error Response Schemaโ
File: server/src/main/java/com/saas/springular/common/exception/ErrorResponse.java (NEW)
Purpose: Standardized error response for all endpoints.
Create error response:
package com.saas.springular.common.exception;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
@Schema(description = "Standard error response")
public class ErrorResponse {
@Schema(description = "Error message", example = "Invalid credentials")
private String message;
@Schema(description = "HTTP status code", example = "401")
private int status;
@Schema(description = "Error timestamp", example = "2025-01-03T12:00:00")
private LocalDateTime timestamp;
@Schema(description = "Request path", example = "/api/auth/login")
private String path;
public ErrorResponse(String message, int status, String path) {
this.message = message;
this.status = status;
this.path = path;
this.timestamp = LocalDateTime.now();
}
// Getters and setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}
Update Global Exception Handler to use ErrorResponse:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ErrorResponse> handleBadCredentials(
BadCredentialsException ex,
HttpServletRequest request
) {
ErrorResponse error = new ErrorResponse(
"Invalid credentials",
HttpStatus.UNAUTHORIZED.value(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
// Other exception handlers...
}
Step 6: Add API Versioningโ
File: server/src/main/java/com/saas/springular/common/config/WebConfig.java
Purpose: Implement API versioning strategy.
Add versioning configuration:
package com.saas.springular.common.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatching(PathMatchConfigurer configurer) {
// Add /v1 prefix to all /api/** endpoints
configurer.addPathPrefix("/api/v1",
c -> c.getPackage().getName().startsWith("com.saas.springular"));
}
}
Update controller mappings:
@RestController
@RequestMapping("/api/v1/auth") // Changed from /api/auth
@Tag(name = "Authentication", description = "Authentication endpoints (v1)")
public class AuthenticationController {
// ...
}
Benefits:
- Future API changes can use /api/v2
- Backward compatibility maintained
- Clear API version in URLs
Step 7: Optional - Add API Contract Testingโ
File: server/build.gradle
Purpose: Add Spring Cloud Contract for API contract testing.
Add dependencies:
plugins {
// ... existing plugins ...
id 'spring-cloud-contract' version '4.1.0'
}
dependencies {
// ... existing dependencies ...
// Spring Cloud Contract
testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
}
contracts {
testFramework = org.springframework.cloud.contract.verifier.config.TestFramework.JUNIT5
baseClassForTests = 'com.saas.springular.ContractTestBase'
contractsDsl {
contractsPath = 'src/test/resources/contracts'
}
}
Create contract base class:
package com.saas.springular;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest
@ActiveProfiles("test")
public class ContractTestBase {
@Autowired
private WebApplicationContext context;
@BeforeEach
public void setup() {
RestAssuredMockMvc.webAppContextSetup(context);
}
}
Create contract file:
// File: src/test/resources/contracts/auth/login.groovy
package contracts.auth
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "should return JWT tokens on successful login"
request {
method POST()
url "/api/v1/auth/login"
headers {
contentType applicationJson()
}
body([
email: "test@example.com",
password: "Test123!@#"
])
}
response {
status 200
headers {
contentType applicationJson()
}
body([
accessToken: anyNonBlankString(),
refreshToken: anyNonBlankString(),
tokenType: "Bearer"
])
}
}
Generate tests:
./gradlew generateContractTests
Run contract tests:
./gradlew test
Verificationโ
Step 1: Verify Swagger UIโ
Access Swagger UI:
http://localhost:8081/swagger-ui/index.html
Expected Result:
- Swagger UI loads successfully
- All endpoints are documented
- Request/response schemas visible
- Example values shown
- "Try it out" functionality works
Step 2: Verify OpenAPI JSONโ
Access OpenAPI JSON:
http://localhost:8081/v3/api-docs
Expected Result:
- Valid OpenAPI 3.0 JSON
- All endpoints documented
- Schemas included
- Security schemes defined
Step 3: Test "Try It Out" Functionalityโ
In Swagger UI:
- Expand
/api/v1/auth/registerendpoint - Click "Try it out"
- Enter test data:
{
"email": "swagger-test@example.com",
"password": "Test123!@#",
"firstName": "Swagger",
"lastName": "Test"
} - Click "Execute"
- Verify response code 201
Expected Result:
- Request executes successfully
- Response shown in Swagger UI
- User created in database
Step 4: Verify API Versioningโ
Test versioned endpoints:
curl -X POST http://localhost:8081/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Test123!@#"}'
Expected Result:
- Request succeeds with v1 prefix
- Old
/api/auth/loginstill works (or returns 404 if migration complete)
Best Practicesโ
Documentation Guidelinesโ
DO:
- โ Document all public API endpoints
- โ Include request/response examples
- โ Document all possible error responses
- โ Use descriptive summaries and descriptions
- โ Group related endpoints with @Tag
- โ Include authentication requirements
DON'T:
- โ Expose internal/debug endpoints in docs
- โ Show stack traces in error responses
- โ Include sensitive information in examples
- โ Duplicate documentation (DRY principle)
Schema Designโ
Consistent Error Format:
{
"message": "Error description",
"status": 400,
"timestamp": "2025-01-03T12:00:00",
"path": "/api/v1/auth/login"
}
Consistent Success Response (where applicable):
{
"data": { /* actual data */ },
"meta": {
"timestamp": "2025-01-03T12:00:00"
}
}
API Versioning Strategyโ
When to Version:
- Breaking changes to request/response format
- Removing endpoints
- Changing authentication mechanisms
When NOT to Version:
- Adding new optional fields
- Adding new endpoints
- Bug fixes
- Performance improvements
Troubleshootingโ
Swagger UI Shows No Endpointsโ
Issue: Swagger UI loads but shows no endpoints.
Solution:
- Verify
springdoc.api-docs.enabled: truein application.yml - Check package scan configuration includes controller packages
- Verify controllers have @RestController annotation
- Check browser console for errors
Endpoints Not Showing in Swagger UIโ
Issue: Some endpoints missing from Swagger UI.
Solution:
- Verify controllers are in scanned packages
- Check if endpoints are excluded by group configuration
- Verify @RequestMapping paths are correct
- Add @Tag annotation to controller
Contract Tests Failingโ
Issue: Generated contract tests fail.
Solution:
- Verify contract base class is correctly configured
- Check contract file syntax (Groovy DSL)
- Ensure test data matches contract expectations
- Check database state in tests
Next Stepsโ
All Boilerplate Enhancement Features Complete!
Congratulations! You have completed all 10 features of the Boilerplate Enhancement Epic:
- โ Database Schema & Validation
- โ Production Infrastructure
- โ Code Quality & Developer Experience
- โ Code Generation Infrastructure
- โ Test Infrastructure
- โ Security Hardening
- โ Reliability Improvements
- โ Configuration Cleanup
- โ Test Coverage Enhancement
- โ API Contract & Documentation
Your Springular boilerplate is now:
- ๐ Secure (CORS, JWT, CSRF, passwords, rate limiting)
- ๐ก๏ธ Reliable (transactions, proper error handling, null safety)
- โ๏ธ Production-ready (correct URLs, environment variables, monitoring)
- ๐งช Well-tested (unit, integration, API tests)
- ๐ Well-documented (OpenAPI specs, Swagger UI)
Summaryโ
This implementation provides:
- โ OpenAPI 3.0 Specification: All endpoints documented
- โ Swagger UI: Interactive API documentation
- โ Schema Validation: Request/response schemas
- โ API Versioning: v1 prefix for all endpoints
- โ Error Standardization: Consistent error response format
- โ Optional Contract Testing: Spring Cloud Contract setup
API documentation is critical for frontend/backend collaboration and preventing breaking changes.