Skip to main content

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:


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 endpoint
  • swagger-ui.path: /swagger-ui - Swagger UI path
  • operations-sorter: method - Sort by HTTP method
  • tags-sorter: alpha - Sort tags alphabetically
  • display-request-duration: true - Show request duration in UI
  • show-actuator: false - Hide actuator endpoints from docs
  • group-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:

  • @Schema on class - Describes the DTO
  • @Schema on fields - Describes each field with example
  • example - Shown in Swagger UI "Try it out"
  • required - Indicates required fields
  • minLength, 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:

  1. Expand /api/v1/auth/register endpoint
  2. Click "Try it out"
  3. Enter test data:
    {
    "email": "swagger-test@example.com",
    "password": "Test123!@#",
    "firstName": "Swagger",
    "lastName": "Test"
    }
  4. Click "Execute"
  5. 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/login still 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:

  1. Verify springdoc.api-docs.enabled: true in application.yml
  2. Check package scan configuration includes controller packages
  3. Verify controllers have @RestController annotation
  4. Check browser console for errors

Endpoints Not Showing in Swagger UIโ€‹

Issue: Some endpoints missing from Swagger UI.

Solution:

  1. Verify controllers are in scanned packages
  2. Check if endpoints are excluded by group configuration
  3. Verify @RequestMapping paths are correct
  4. Add @Tag annotation to controller

Contract Tests Failingโ€‹

Issue: Generated contract tests fail.

Solution:

  1. Verify contract base class is correctly configured
  2. Check contract file syntax (Groovy DSL)
  3. Ensure test data matches contract expectations
  4. Check database state in tests

Next Stepsโ€‹

All Boilerplate Enhancement Features Complete!

Congratulations! You have completed all 10 features of the Boilerplate Enhancement Epic:

  1. โœ… Database Schema & Validation
  2. โœ… Production Infrastructure
  3. โœ… Code Quality & Developer Experience
  4. โœ… Code Generation Infrastructure
  5. โœ… Test Infrastructure
  6. โœ… Security Hardening
  7. โœ… Reliability Improvements
  8. โœ… Configuration Cleanup
  9. โœ… Test Coverage Enhancement
  10. โœ… 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.