Why spend days building API clients manually when you can auto-generate them in minutes and keep them perfectly in sync?
Introduction#
As backend developers, we frequently build APIs with the intention that others can easily consume them - whether in other applications, end-to-end tests, or stress tests. But why not make life easier for everyone and generate the clients ourselves? The traditional approach forces API consumers to manually write HTTP clients, parse responses, handle errors, and maintain code that mirrors your API structure. Every time your API changes, they need to update their client code. This creates unnecessary friction and maintenance overhead. What if we could eliminate this burden entirely? What if consuming your API was as simple as adding a dependency and calling type-safe methods?
The Idea#
We invest significant time crafting our controllers, OpenAPI specifications, and API implementations. Once our API is working, we want to write end-to-end tests, create stress tests, or ensure other teams and organizations can easily integrate with our services. So why rebuild a client from scratch when we can generate it automatically? While our technology stack might be fixed - perhaps we’re using Spring Boot or Quarkus - the beauty of client generation is that it’s framework-agnostic. This means anyone can use our generated clients, regardless of their technology choices.
My goal was to create the lightest possible client that’s easy to use and maintain. The result? A solution that:
- Eliminates manual client code
- Ensures type safety
- Automatically stays in sync with your API
- Works with any Java application
- Requires minimal dependencies
How It Works#
To generate a client, we need an OpenAPI specification as our source of truth. There are two common approaches to obtaining this specification:
Contract-First Approach:
- Write your OpenAPI specification manually
- Generate controllers and models from the spec
- Implement the business logic
Code-First Approach:
- Write your controllers with annotations
- Use tools like SpringDoc or Swagger to generate the OpenAPI spec automatically
- Use the generated spec for client generation
Once you have your OpenAPI specification, the client generation process is straightforward:
- Configure the generator - Choose your target language, set package names, and configure code style options
- Run the build - Execute the generation as part of your Maven or Gradle build
- Publish the artifact - Deploy the generated client to your Maven repository (Nexus, Artifactory, or Maven Central)
- Consume - Other developers simply add your client as a dependency
The OpenAPI Generator Maven plugin handles all the heavy lifting. It reads your specification and generates:
- Model classes (POJOs) for request and response objects
- API interface definitions with all endpoints
- HTTP client implementation using libraries like OkHttp
- Serialization/deserialization logic
- Exception handling
- Documentation
Example Implementation#
Let’s walk through a complete example of generating a client for a Person API.
The OpenAPI Specification#
Here’s our API specification for managing persons. This could be hand-written or auto-generated from your controllers:
openapi: 3.0.3
info:
title: Person API
version: 1.0.0
description: Demo API for managing persons
paths:
/person:
get:
summary: Find all persons
operationId: findAllPersons
responses:
'200':
description: List of persons
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PersonResponseResource'
post:
summary: Create a new person
operationId: createPerson
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PersonRequestResource'
responses:
'201':
description: Person created
content:
application/json:
schema:
$ref: '#/components/schemas/PersonResponseResource'
/person/{id}:
get:
summary: Find person by ID
operationId: findPersonById
parameters:
- in: path
name: id
required: true
schema:
type: integer
responses:
'200':
description: Found person
content:
application/json:
schema:
$ref: '#/components/schemas/PersonResponseResource'
'404':
description: Not found
put:
summary: Update a person
operationId: updatePerson
parameters:
- in: path
name: id
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PersonRequestResource'
responses:
'200':
description: Person updated
content:
application/json:
schema:
$ref: '#/components/schemas/PersonResponseResource'
'404':
description: Not found
components:
schemas:
PersonResponseResource:
type: object
properties:
id:
type: string
format: uuid
example: b79cafbb-5a70-455e-93d2-62e744c68379
name:
type: string
example: Nicholas Meyers
age:
type: integer
example: 30
PersonRequestResource:
type: object
properties:
name:
type: string
example: Nicholas Meyers
age:
type: integer
example: 30
required:
- name
- age
Maven Configuration#
This POM file configures the OpenAPI Generator plugin and includes all necessary dependencies. The key is the openapi-generator-maven-plugin configuration:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>be.nicholasmeyers</groupId>
<artifactId>demo-client-generator</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-client-generator</name>
<description>Auto-generated client for Person API</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<!-- Logging for HTTP requests/responses -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>4.12.0</version>
</dependency>
<!-- JSON serialization -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- Enhanced Gson features -->
<dependency>
<groupId>io.gsonfire</groupId>
<artifactId>gson-fire</artifactId>
<version>1.9.0</version>
</dependency>
<!-- Jakarta annotations -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<!-- Package sources -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Package javadocs -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<failOnError>false</failOnError>
<doclint>none</doclint>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- OpenAPI Generator -->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.2.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<!-- Path to your OpenAPI spec -->
<inputSpec>${project.basedir}/src/main/resources/person-open-api-spec.yaml</inputSpec>
<!-- Target language -->
<generatorName>java</generatorName>
<!-- Generate model classes -->
<generateModels>true</generateModels>
<modelPackage>be.nicholasmeyers.democlientgenerator.client.model</modelPackage>
<!-- Generate API interfaces -->
<generateApis>true</generateApis>
<apiPackage>be.nicholasmeyers.democlientgenerator.client.api</apiPackage>
<!-- Configuration options -->
<configOptions>
<library>okhttp-gson</library>
<dateLibrary>java8</dateLibrary>
<useJakartaEe>true</useJakartaEe>
<interfaceOnly>false</interfaceOnly>
<skipDefaultInterface>false</skipDefaultInterface>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Key Configuration Options Explained:
inputSpec: Location of your OpenAPI specification filegeneratorName: Target language (java, typescript-axios, python, etc.)library: HTTP client library to use (okhttp-gson, native, apache-httpclient)modelPackage: Package name for generated model classesapiPackage: Package name for generated API classesuseJakartaEe: Use Jakarta EE annotations instead of javaxdateLibrary: How to handle date/time types (java8 uses LocalDate/LocalDateTime)
Using the Generated Client#
After running mvn clean install, the generated client is ready to use. Here’s how simple it becomes:
package be.nicholasmeyers.example;
import be.nicholasmeyers.democlientgenerator.client.api.PersonApi;
import be.nicholasmeyers.democlientgenerator.client.model.PersonRequestResource;
import be.nicholasmeyers.democlientgenerator.client.model.PersonResponseResource;
import be.nicholasmeyers.democlientgenerator.client.ApiClient;
import be.nicholasmeyers.democlientgenerator.client.ApiException;
import java.util.List;
public class PersonClientExample {
private final PersonApi personApi;
public PersonClientExample(String baseUrl) {
// Configure the API client
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(baseUrl);
// Optionally add authentication
// apiClient.setApiKey("your-api-key");
// apiClient.setBearerToken("your-jwt-token");
this.personApi = new PersonApi(apiClient);
}
public List<PersonResponseResource> getAllPersons() throws ApiException {
// Simple method call - no manual HTTP handling needed
return personApi.findAllPersons();
}
public PersonResponseResource getPersonById(Integer id) throws ApiException {
return personApi.findPersonById(id);
}
public PersonResponseResource createPerson(String name, Integer age) throws ApiException {
PersonRequestResource request = new PersonRequestResource();
request.setName(name);
request.setAge(age);
return personApi.createPerson(request);
}
public PersonResponseResource updatePerson(Integer id, String name, Integer age) throws ApiException {
PersonRequestResource request = new PersonRequestResource();
request.setName(name);
request.setAge(age);
return personApi.updatePerson(id, request);
}
public static void main(String[] args) {
try {
PersonClientExample example = new PersonClientExample("http://localhost:8080");
// Create a new person
PersonResponseResource created = example.createPerson("John Doe", 25);
System.out.println("Created person with ID: " + created.getId());
// Fetch all persons
List<PersonResponseResource> allPersons = example.getAllPersons();
System.out.println("Total persons: " + allPersons.size());
// Get specific person
PersonResponseResource person = example.getPersonById(1);
System.out.println("Found person: " + person.getName());
} catch (ApiException e) {
System.err.println("API error: " + e.getMessage());
System.err.println("Response body: " + e.getResponseBody());
}
}
}
Notice how clean this is compared to manually writing HTTP clients with RestTemplate or HttpClient. No URL construction, no manual JSON parsing, no response mapping - it’s all handled automatically.
Advanced Use Cases#
Adding Authentication Interceptors:
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class BearerTokenInterceptor implements Interceptor {
private final String token;
public BearerTokenInterceptor(String token) {
this.token = token;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + token)
.build();
return chain.proceed(request);
}
}
// Usage
ApiClient apiClient = new ApiClient();
apiClient.getHttpClient().newBuilder()
.addInterceptor(new BearerTokenInterceptor("your-jwt-token"))
.build();
Creating Framework-Specific Starters:
You can wrap the generated client in framework-specific starters:
- Spring Boot Starter: Auto-configure the client with Spring properties
- Quarkus Extension: Create a Quarkus-native client extension
- Micronaut Integration: Use Micronaut’s dependency injection
Multi-Client Generation:
Generate clients for multiple languages from the same spec:
<executions>
<execution>
<id>java-client</id>
<goals><goal>generate</goal></goals>
<configuration>
<generatorName>java</generatorName>
<outputDirectory>${project.basedir}/target/java-client</outputDirectory>
</configuration>
</execution>
<execution>
<id>typescript-client</id>
<goals><goal>generate</goal></goals>
<configuration>
<generatorName>typescript-axios</generatorName>
<outputDirectory>${project.basedir}/target/typescript-client</outputDirectory>
</configuration>
</execution>
</executions>
Conclusion#
Generating API clients is no longer a luxury - it’s a best practice that saves time, reduces errors, and improves developer experience. With just a few lines of Maven configuration, you can provide type-safe, well-documented clients to all your API consumers.
The benefits compound over time. Every API change is automatically reflected in the client. Every new endpoint becomes immediately available. Every consumer gets the same high-quality experience.
Whether you’re building microservices, providing APIs to external partners, or writing integration tests, client generation should be part of your workflow. It’s fast, reliable, and eliminates entire classes of integration bugs.
Next Steps:
- Add OpenAPI documentation to your existing APIs
- Set up client generation in your build pipeline
- Publish your first generated client to your Maven repository
- Create organization-wide conventions for client generation
- Explore language-specific generators for frontend teams
Resources:
- OpenAPI Generator Documentation
- OpenAPI Specification
- SpringDoc for Spring Boot
- Maven Plugin Configuration
With minimal effort, you can transform how your organization builds and consumes APIs. Start generating clients today, and never write manual HTTP client code again.