Spring Boot Topic
OpenAPI as Contract-First Development with Spring Boot
What is OpenAPI?
OpenAPI is a specification for describing RESTful APIs in a standardized, language-agnostic format (YAML or JSON). It serves as a contract between API providers and consumers, defining endpoints, request/response formats, authentication, and more.
Key Benefits:
-
Documentation: Auto-generates interactive API docs
-
Code Generation: Creates client SDKs and server stubs
-
Validation: Enforces API consistency
-
Testing: Enables contract testing
-
Collaboration: Serves as single source of truth
OpenAPI as Contract-First Development
Contract-First Approach:
-
Design API first using OpenAPI specification
-
Review/approve contract with stakeholders
-
Generate code from the contract
-
Implement the generated interfaces
Example: Movie API OpenAPI Contract
yaml
openapi: 3.0.3
info:
title: Movie API
description: REST API for managing movies
version: 1.0.0
servers:
- url: http://localhost:8080/api
description: Development server
paths:
/movies:
get:
summary: Get all movies
responses:
'200':
description: List of movies
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Movie'
post:
summary: Create a new movie
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MovieRequest'
responses:
'201':
description: Movie created
content:
application/json:
schema:
$ref: '#/components/schemas/Movie'
/movies/{id}:
get:
summary: Get movie by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Movie found
content:
application/json:
schema:
$ref: '#/components/schemas/Movie'
'404':
description: Movie not found
components:
schemas:
Movie:
type: object
properties:
id:
type: integer
example: 1
title:
type: string
example: "Inception"
year:
type: integer
example: 2010
director:
type: string
example: "Christopher Nolan"
genre:
type: string
example: "Sci-Fi"
required:
- id
- title
- year
MovieRequest:
type: object
properties:
title:
type: string
example: "Inception"
year:
type: integer
example: 2010
director:
type: string
example: "Christopher Nolan"
genre:
type: string
example: "Sci-Fi"
required:
- title
- year
Spring Boot Implementation
1. Add Dependencies (pom.xml)
xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.5.0</version>
</dependency>
2. Generate Code from OpenAPI Spec
Add Maven plugin configuration:
xml
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/movie-api.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>com.example.movie.api</apiPackage>
<modelPackage>com.example.movie.model</modelPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
3. Implement the Generated Interface
Movie Controller Implementation:
java
@RestController
@RequestMapping("/api")
public class MovieController implements MoviesApi {
private final MovieService movieService;
private final ModelMapper modelMapper;
public MovieController(MovieService movieService) {
this.movieService = movieService;
this.modelMapper = new ModelMapper();
}
@Override
public ResponseEntity<List<Movie>> getMovies() {
List<MovieDTO> movies = movieService.getAllMovies();
List<Movie> apiMovies = movies.stream()
.map(dto -> modelMapper.map(dto, Movie.class))
.collect(Collectors.toList());
return ResponseEntity.ok(apiMovies);
}
@Override
public ResponseEntity<Movie> getMovieById(Integer id) {
return movieService.getMovieById(id)
.map(dto -> ResponseEntity.ok(modelMapper.map(dto, Movie.class)))
.orElse(ResponseEntity.notFound().build());
}
@Override
public ResponseEntity<Movie> createMovie(MovieRequest movieRequest) {
MovieDTO movieDTO = modelMapper.map(movieRequest, MovieDTO.class);
MovieDTO savedDTO = movieService.createMovie(movieDTO);
Movie response = modelMapper.map(savedDTO, Movie.class);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
4. Service Layer
java
@Service
public class MovieService {
private final Map<Integer, MovieDTO> movies = new ConcurrentHashMap<>();
private final AtomicInteger idCounter = new AtomicInteger(1);
public List<MovieDTO> getAllMovies() {
return new ArrayList<>(movies.values());
}
public Optional<MovieDTO> getMovieById(Integer id) {
return Optional.ofNullable(movies.get(id));
}
public MovieDTO createMovie(MovieDTO movieDTO) {
Integer id = idCounter.getAndIncrement();
movieDTO.setId(id);
movies.put(id, movieDTO);
return movieDTO;
}
}
5. DTO Classes
java
@Data
public class MovieDTO {
private Integer id;
private String title;
private Integer year;
private String director;
private String genre;
}
6. Configure OpenAPI UI
java
@Configuration
public class OpenAPIConfig {
@Bean
public OpenAPI movieOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Movie API")
.description("REST API for managing movies")
.version("1.0.0"))
.addServersItem(new Server()
.url("/api")
.description("API Server"));
}
}
Development Workflow
Step 1: Design Contract
bash
# Create OpenAPI spec
src/main/resources/api/movie-api.yaml
Step 2: Generate Code
bash
mvn clean compile
# Generates:
# - MoviesApi.java (interface)
# - Movie.java (model)
# - MovieRequest.java (model)
Step 3: Implement Business Logic
-
Implement the generated
MoviesApiinterface -
Add service layer
-
Add data access layer
Step 4: Test & Validate
java
@SpringBootTest
@AutoConfigureMockMvc
class MovieControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void createMovie_ShouldReturnCreated() throws Exception {
String movieJson = """
{
"title": "Inception",
"year": 2010,
"director": "Christopher Nolan",
"genre": "Sci-Fi"
}
""";
mockMvc.perform(post("/api/movies")
.contentType(MediaType.APPLICATION_JSON)
.content(movieJson))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.title").value("Inception"));
}
}
Step 5: Access Documentation
-
Swagger UI:
http://localhost:8080/swagger-ui.html -
OpenAPI JSON:
http://localhost:8080/v3/api-docs
Contract Testing
java
@ContractTest
public class MovieApiContractTest {
@Test
void shouldAdhereToOpenAPISpec() {
// Use tools like Spring Cloud Contract or Pact
// to verify implementation matches spec
}
}
Best Practices
-
Version Control: Store OpenAPI spec in version control
-
Validation: Use OpenAPI validators during CI/CD
-
Client Generation: Generate client SDKs for consumers
-
Backward Compatibility: Follow semantic versioning
-
Documentation: Use OpenAPI extensions for better docs
Tools Integration
-
Swagger Editor: Design/validate OpenAPI specs
-
OpenAPI Generator: Generate code in multiple languages
-
Postman: Import OpenAPI for testing
-
GitHub Actions: Validate spec on PRs
Benefits in Microservices
-
Consistency: Uniform API design across services
-
Parallel Development: Frontend/backend can work simultaneously
-
Automation: Reduces boilerplate code
-
Quality: Early detection of design issues
This contract-first approach ensures your API is well-designed, documented, and consistent before implementation begins, reducing integration issues and improving developer experience.
Comments (0)
No comments yet. Be the first to comment!