1. Introduction
APIs are one of the most important components in communication between services in modern software, and the OpenAPI
Specification has become
the go-to standard for defining them. But while building an API might seem straightforward, offering a smooth developer
experience often depends on the availability of SDKs (Software Development Kits). SDKs simplify API integration by
providing pre-built, language-specific libraries that developers can use to interact with APIs more efficiently.
Creating and maintaining SDKs manually for multiple programming languages can quickly become a time-consuming task.
That’s where automating the process as much as possible comes in handy. By leveraging the OpenAPI standard, you can
automate the generation of SDKs, reducing maintenance time and ensuring consistency across different platforms.
In this guide, we’ll focus on how to build a Java SDK based on an OpenAPI specification. By the end of this
tutorial, you’ll have a clear understanding of the following:
- How to build a Java SDK based on an OpenAPI specification?
- What are the necessary steps to go from an OpenAPI file to a full functional, deployable Java SDK?
- What are the best practices for building and maintaining SDKs?
We’ll use Corbado’s passkey-first Java SDK as an example, guiding you through
the practical steps and considerations
involved in creating a professional, user-friendly SDK.
2. Prerequisites
The prerequisites are pretty simple. You need a backend services that exposes an API. This API should be a RESTful API
and you should have an OpenAPI specification of it available.
3. Use an API Generator to Create a Client
The first step in building a Java SDK from an OpenAPI specification is to use an OpenAPI generator to create a
client SDK. The OpenAPI generator simplifies the process by automatically generating client-side code that interacts
with your API.
Ideally, the generated client SDK should be placed in a separate project or repository. This ensures clean
separation from your main application, helping to simplify dependency management. Generated SDKs often include
third-party libraries that might not align with the dependencies used in your core project.
Best Practice: Avoid Modifying Generated Code
It’s best practice to avoid manually modifying the generated code. Once you start editing the generated SDK code, it
becomes difficult to keep up with future updates or regenerate the SDK when your OpenAPI specification changes. Instead,
any custom functionality should be built on top of the generated code, not directly within it.
If you’re concerned about exposing the complexities of the generated code to developers, decouple the generated code
from your SDK interface. This way, the complexity remains hidden, and your SDK provides a clean, simple interface for
developers to use.
4. Understand the SDK Requirements
When building a Java SDK from an OpenAPI specification, it’s important to first familiarize yourself with existing SDKs
that can serve as role models (if they exist – in other cases you need to define the requirements for the SDK).
4.1 Key Use Cases: Session Validation and User Information Extraction
For Corbado’s Java Passkeys SDKs, two key scenarios include session validation and extracting additional user information.
These features are important for developers integrating user authentication into their applications.
4.2 Purpose of the SDK: Why Not Just Use a Generated Client?
The goal of the SDK is to provide more than just a generated client. While a simple client offers basic API interaction,
a well-built SDK enhances the developer experience by offering:
- A simple, easy-to-install library for connecting to the backend.
- Meaningful error handling, making it easier for developers to debug and resolve issues.
- Easy configuration, allowing developers to get started with minimal friction.
- Clear, documented examples for all available functionality (often provided through unit tests).
- A lower entry barrier for developers trying out the service for the first time.
4.3 JWT/JWK Handling and Validation
An essential part of building the Corbado SDK is understanding how JSON Web Tokens (JWT)
and JSON Web Keys (JWK) are
handled, as these are used for session validation. You’ll want to:
- Familiarize yourself with JWT/JWK concepts
- Search for available JWT libraries for Java
A JWT validation process could include validating the signature, checking token expiration, and handling JWK
errors with custom exceptions like JWTVerificationException
or JwkException
.
private SessionValidationResult getAndValidateUserFromShortSessionValue(final String shortSession)
throws JWTVerificationException, JwkException, IncorrectClaimException {
if (shortSession == null || shortSession.isEmpty()) {
throw new IllegalArgumentException("Session value cannot be null or empty");
}
try {
// Decode the JWT without verifying it, to extract its claims and headers.
DecodedJWT decodedJwt = JWT.decode(shortSession);
// Retrieve the signing key (JWK) using the Key ID (kid) from the JWT header.
final Jwk jwk = this.jwkProvider.get(decodedJwt.getKeyId());
// If the signing key (JWK) is not found, throw a custom exception.
if (jwk == null) {
throw new SigningKeyNotFoundException(shortSession, null);
}
// Extract the RSA public key from the JWK for verifying the JWT.
final RSAPublicKey publicKey = (RSAPublicKey) jwk.getPublicKey();
// Define the RSA256 algorithm using the retrieved public key.
final Algorithm algorithm = Algorithm.RSA256(publicKey);
// Create a JWT verifier using the algorithm and expected issuer.
final JWTVerifier verifier = JWT.require(algorithm).withIssuer(this.issuer).build();
// Verify the JWT signature and decode the JWT.
decodedJwt = verifier.verify(shortSession);
// Build and return the session validation result, extracting claims like name and userID.
return SessionValidationResult.builder()
.fullName(decodedJwt.getClaim("name").asString()) // Extract the 'name' claim.
.userID(decodedJwt.getClaim("sub").asString()) // Extract the 'sub' (user ID) claim.
.build();
} catch (final IncorrectClaimException e) {
// Handle cases where the JWT's claims do not match expected values (e.g., incorrect issuer).
// If the claim that caused the exception is the 'iss' (issuer) claim.
if (StringUtils.equals(e.getClaimName(), "iss")) {
final String message =
e.getMessage()
+ " Be careful of the case where issuer does not match. "
+ "You have probably forgotten to set the cname in config class.";
// Rethrow the IncorrectClaimException with the updated message.
throw new IncorrectClaimException(message, e.getClaimName(), e.getClaimValue());
}
// If the exception is not related to the issuer, rethrow the original exception.
throw e;
} catch (final JwkException | JWTVerificationException e) {
// Catch any errors related to obtaining the JWK or verifying the JWT.
// Rethrow these exceptions as they are critical and need to be handled by the caller.
throw e;
}
}
Example endpoint in Spring Boot using Session Service:
@RequestMapping("https://dev.to/profile")
public String profile(
final Model model, @CookieValue("cbo_short_session") final String cboShortSession) {
try {
// Validate user from token
final SessionValidationResult validationResp =
sdk.getSessions().getAndValidateCurrentUser(cboShortSession);
// get list of emails from identifier service
List<Identifier> emails;
emails = sdk.getIdentifiers().listAllEmailsByUserId(validationResp.getUserID());
model.addAttribute("PROJECT_ID", projectID);
model.addAttribute("USER_ID", validationResp.getUserID());
model.addAttribute("USER_NAME", validationResp.getFullName());
// select email of your liking or list all emails
model.addAttribute("USER_EMAIL", emails.get(0).getValue());
} catch (final Exception e) {
//Handle verification errors here
model.addAttribute("ERROR", e.getMessage());
return "error";
}
return "profile";
}
Providing clear unit tests for valid and invalid tokens can demonstrate how these validations work in practice. This
not only ensures robustness but also offers developers a clear example of how to handle JWTs in their applications. You
can take a look
at SessionServiceTest
as an example.
4.4 Handling Errors in the SDK
Error handling is crucial to any SDK. Some of the key exceptions in our example include:
-
CorbadoServerException: Thrown when the backend API returns an HTTP status code other than 200, this exception
includes deserialized error responses, making additional error information easily accessible. - StandardException: Used for client-side errors, unrelated to the backend.
- JWTVerificationException and JwkException: Specific to session validation errors related to JWT or JWK.
4.5 Implementing Client-Side Validation
The SDK also takes care of validating input on the client side to avoid sending invalid data to the server. Common
validations include:
- URL validation: Ensuring that provided URLs follow the correct format.
- String constraints: For instance, ensuring that user IDs are not empty.
-
Configuration validation: Certain fields, like projectId and apiSecret, should follow specific patterns (e.g.,
projectId starting withpro-
and apiSecret starting withcorbado1_
).
5. Identify Best Practices for Java
Follow Java best practices for project structure and code quality is essential to ensure that your
library is easy to use, maintain, and integrate. Below are some recommendations for structuring your SDK project and
maintaining clean, readable code, with specific examples for Java.
5.1 Structuring Your Project for Ease of Use
A well-structured project is crucial for making your Java SDK standalone and easily importable by other developers.
Look at this repository on GitHub, which provides sample
project
layouts that follow best practices. Organizing your SDK into
modular components with clear directory structures helps developers navigate the code more easily and import your SDK
without hassle.
5.2 Use Modern Formatters and Linters
To maintain clean, standardized code, it’s important to use modern formatters and linters. These tools ensure that your
code adheres to established guidelines, making it more readable, consistent, and less prone to errors. Below is a
recommendation for Java:
-
Checkstyle with Google Java Style: Ensures your code
follows Google’s Java style guidelines. -
SonarLint: Identifies potential bugs and code smells in your
Java code, improving overall code quality. - FindBugs: Detects potential issues in Java code that could lead to bugs.
-
google-java-format: A tool
that reformats your code according to Google’s Java style, ensuring a
clean,
consistent codebase.
5.3 Keep Code Clean and Maintainable
Clean code is critical for any SDK, but it becomes even more important in smaller projects where every line of code has
the potential to impact usability and maintainability.
6. Identify Best Practices and Established Coding Standards for SDK Development
To build a nice Java SDK, look at how established companies approach SDK development. For example, Stripe is known for
having one of the most well-documented and widely used SDKs across multiple languages. Studying their approach can teach
you the following:
-
CI/CD Structure: Stripe uses an efficient continuous integration and continuous delivery (CI/CD) pipeline to
automate tests and deployment. Understanding their setup can help you build a basic CI/CD structure that ensures your
SDK is always tested and ready for production. -
What to Test in CI/CD: By observing what major SDKs like Stripe include in their test suite, you’ll learn what is
critical for ensuring compatibility, such as unit tests, integration tests, and API contract validation. Testing
should cover core functionality, edge cases, and backward compatibility. -
Language Version Compatibility: Ensuring compatibility with multiple versions of your target language is essential
for a widely used SDK. For Java, you might support both Java 8 and above, depending on user requirements. -
Linting and Code Static Analysis: Stripe implements strict linting and code static analysis to maintain high code
quality. Tools like Checkstyle for Java can be integrated into your CI
pipeline to catch
potential issues before they reach production.
6.1 Code and Project Structure
Examining how SDKs like Stripe structure their projects can give you insights into creating a clean, modular project
architecture. For example, in Java, Stripe uses
the Builder Pattern for their client configuration
classes, which is
particularly useful when dealing with many optional parameters. This pattern ensures that your SDK’s API remains
flexible while maintaining readability and simplicity for developers. If your SDK has configuration-heavy components,
adopting this pattern might be ideal.
6.2 Documentation Best Practices
One of the key takeaways from analyzing successful SDKs is the importance of comprehensive and accessible
documentation. The README file is often the first
interaction a developer has with your SDK, so it should include:
- Clear setup instructions: How to install and configure the SDK with minimal friction.
- Code examples: Demonstrating core use cases, such as authentication, session handling, and error management.
-
Links to further documentation: For developers who need more detailed instructions or wish to explore advanced use
cases.
7. Determine Your Tech Stack and Useful Libraries
Building an SDK requires selecting the right libraries to ensure compatibility, maintainability, and ease of use. The
stack you choose will directly affect how well your SDK integrates into other projects and how easy it is for developers
to adopt it. Let’s break down the key components you’ll need to consider based on the insights gathered from previous
sections and established best practices.
7.1 Build Tools and Dependency Management
For Java, the two main build tools to consider are Maven and Gradle. Both are widely used, but your choice
depends on the specific needs of your SDK:
-
Maven: If you’re more familiar with Maven and don’t need custom build logic, it’s a
solid, well-established
option. Maven is known for its simplicity, strong dependency management, and extensive ecosystem. It integrates easily
with package distribution platforms like Maven Central.
-
Gradle: Gradle offers more flexibility and faster build times, especially for larger
projects. It can be useful if your
SDK requires more advanced build customizations or if you prioritize build performance.
7.2 HTTP Client and JSON Parsing
Your SDK will need a reliable HTTP client to interact with APIs. For Java,
consider HttpClient
from the standard
library or popular libraries like OkHttp if you need more flexibility.
For JSON parsing, both Jackson and Gson
are widely used in Java. Jackson is often favored for its
extensive features and performance, while Gson is lighter and simpler to use. If your SDK needs advanced
serialization features or works with complex JSON structures, Jackson is likely the better choice.
7.3 Logging and Error Handling
For logging, Java developers often choose between SLF4J with Logback
or Log4j. SLF4J provides an abstraction
layer that allows you to switch between logging implementations without changing your code. Coupled with Logback, it
offers high performance and flexibility. Log4j is another solid option but has become less popular due to past
security vulnerabilities.
When handling errors, it’s important to consider how your SDK will interact with external systems. For example, using a
custom exception like CorbadoServerException
can provide meaningful error messages when something goes wrong during
API
calls. This is especially useful when developers need to debug integrations with your SDK.
7.4 Testing Frameworks
Testing is critical to maintaining the reliability of your SDK. For Java, tools
like JUnit and Mockito are
standard for unit testing and mocking. JUnit provides a simple, structured way to write tests, while Mockito allows you
to mock objects in tests, which is particularly useful for API-driven SDKs where you need to simulate API responses.
7.5 CI/CD Integration
Continuous integration and delivery (CI/CD) are essential for ensuring that your SDK is always deployable and free of
bugs. Popular options include:
-
GitHub Actions: Integrated into GitHub, making it a convenient option if your SDK is hosted there. It’s flexible,
highly customizable, and free for open-source projects. - Jenkins: A self-hosted solution that provides more control but requires additional setup and maintenance.
- Travis CI: A cloud-based CI/CD service that’s particularly popular with open-source projects.
Whichever you choose, make sure your pipeline includes automated testing, linting, and code analysis.
For Corbado’s Java SDK, we used the following CI/CD process:
The SDK should be compatible with most used versions of the language. CI/CD can easily do that with matrix feature
that allows you to run multiple jobs in parallel with different configurations.
This often helps identify compatibility or configuration issues that may not be apparent locally if the build or
tests are failing.
Simplify the deployment process for other developers by following these steps: After build, lint and test you can choose
to deploy the application on some events (for example on version
tag in Java SDK). No extra steps needed. For Java, we needed to:
-
Register
at Central Portal. - Create a GPG Key to sign the build.
- Create a deployment job in the GitHub Actions workflow.
- Make sure the projects comply
with Central requirements. - Follow Centrals publishing guide for Maven
- Configure GPG
- Make a temporary Maven set up (needed by central publishing
plugin) - Sign and publish the artifact
7.6 Optional Tools
Libraries like Lombok can improve development efficiency and reduce boilerplate code. It can greatly reduce
boilerplate code by automatically generating getters, setters,
constructors, and builders at compile time using annotations.
7.7 Dependency Management
Managing dependencies is a key aspect of maintaining a clean, lightweight SDK. Avoid adding unnecessary dependencies, as
they can bloat your package and potentially introduce security vulnerabilities. Be mindful of separating compile-time, runtime, and test dependencies. For example, in Maven, use the
tag to define when a dependency is
required.
7.8 Security and Compatibility Concerns
When selecting libraries, also take into account security vulnerabilities and version compatibility. For
example, older versions of dependencies may no longer receive security updates, which can introduce risks to your users.
Be cautious with which versions of dependencies you support and ensure that your SDK runs on the most commonly used
versions of the language.
8. Ease of Collaboration and Development
When building an SDK, ensuring a smooth and collaborative development process is key to long-term success. This requires
setting up consistent configurations, automating processes where possible, and simplifying release management to make it
easy for teams to work together and ship updates.
8.1 Consistent Project Configuration Across All Machines
A full CI/CD pipeline is essential for ensuring that all code is automatically tested and analyzed before it reaches
production. However, to make collaboration as seamless as possible, every developer working on the SDK should use the
same coding standards and tools, regardless of their machine or development environment.
By sharing these configuration files through version control, developers won’t need to reconfigure their IDEs or tools
every time they clone the project. This ensures consistency in code quality, reduces friction in onboarding new
developers, and prevents errors caused by mismatched environments.
8.2 Sharing IDE Configuration
If possible, provide IDE-specific configuration files for your language and preferred IDEs, such as Eclipse, IntelliJ IDEA, or VS Code. This will help ensure that developers are using the same environment settings, making
collaboration more efficient. For Java projects, you could include an .idea
folder for IntelliJ IDEA or .project
for
Eclipse in your repository to share project settings.
Providing pre-configured settings files simplifies the development process by ensuring that all contributors work in an
optimized and uniform environment.
8.3 Version Management and Simplified Releases
Managing the SDK version should be straightforward and clear. If your versioning process is complicated or error-prone,
it can lead to mistakes during release preparation, resulting in inconsistencies across different environments.
A best practice is to make the VERSION file the single source of truth for your SDK version. This simplifies the
release process by ensuring that all tooling, documentation, and package metadata pull from the same version reference.
By keeping the version in one place, you reduce the risk of mismatches between what’s deployed and what’s documented.
For example, you can update the VERSION file automatically during your CI/CD pipeline to reflect the latest release
version. This practice can also trigger other actions, such as updating changelogs and pushing the latest version to
package managers like Maven Central.
8.4 Automating Development Workflows
Automation plays a key role in maintaining ease of collaboration. Set up pre-commit hooks that run linters,
formatters, and tests before changes are committed to the codebase. This will ensure that only clean, validated code is
pushed, reducing the chance of introducing errors into the shared repository.
For example:
- Husky can be used to set up Git hooks that run scripts before code is committed.
-
Prettier (for JavaScript) can automatically format code as part of the commit process,
ensuring consistent styling across all commits.
9. Testing
Testing is a critical part of building any SDK, ensuring that it works as expected and is reliable in production
environments.
When building your SDK, it’s efficient to start by referencing existing tests from similar SDKs. For example, if Corbado
already has SDKs written in other languages like PHP, use those as a baseline. You can adapt and extend the existing
test cases to match the language-specific features of your new SDK.
- Start by examining the key use cases covered by the existing tests, such as session validation, error handling,
and JWT verification. - Replicate those tests in your new SDK, making adjustments for language differences.
- If your SDK introduces new functionality, be sure to add additional tests that cover these features.
To ensure that your SDK delivers a great developer experience, it’s essential to approach testing from the perspective
of an external developer who will be using it. Set up the SDK as if you were an external user, integrating it into a
sample application to identify any pain points or unclear areas.
10. Documentation
Clear, concise, and well-structured documentation is crucial to the success of any SDK. It ensures that developers can
easily understand how to implement your SDK, use its features, and troubleshoot issues. The documentation should not
only provide a comprehensive overview of the SDK’s capabilities but also guide developers through its use with
real-world examples.
10.1 Ensure Users Understand Function Arguments and Configuration Fields
Your documentation must clearly outline what is required for each function and configuration field. Developers need to
know:
-
Which arguments are required: Make it clear in the documentation which function parameters or configuration fields
are mandatory and which are optional. Specify the default values for any optional parameters, if applicable. -
What constraints apply to each field: For example, is the field nullable? Are there maximum or minimum length
requirements? Is the field expected to follow a certain format (e.g., email, URL)? These details should be explicitly
stated to avoid confusion and potential errors during implementation.
To ensure consistency, the information in your documentation should be automatically generated from your code
annotations, such as Javadoc for Java. This ensures that the documentation is always up-to-date and accurately reflects the SDK’s implementation.
10.2 Clearly Explain Functions, Classes, and Configuration Parameters
Your documentation should provide detailed explanations of what each function and class does. This includes:
- The purpose of the function or class.
- What parameters it expects and their types.
- The expected output or return type.
- Any side effects the function may have (e.g., database changes, API calls).
- Specific error conditions or exceptions the user should handle.
By providing clear and descriptive documentation, you lower the barrier to entry for developers who are new to your SDK,
making it easier for them to understand how to integrate it into their projects.
10.3 Follow Standard Code Commenting Practices
Beyond formal documentation, clear code comments are essential to help future developers (including your team)
understand the logic behind certain code decisions. Use standard commenting practices in your codebase to:
- Explain complex logic or edge cases that might not be immediately obvious.
- Mark TODOs or areas that need further attention.
- Provide additional context when handling errors or exceptions.
Good commenting practices help bridge the gap between the code and its documentation, making the project easier to
maintain and extend.
11. Conclusion
Building a Java SDK from an OpenAPI specification involves several key steps, from generating the client to ensuring
ease of use for developers through proper project structure, best practices, and thorough testing. By focusing on a
clean, consistent technology stack, adopting industry-standard tools and libraries, and providing robust documentation,
you create an SDK that’s intuitive, reliable, and easy to integrate. Following these best practices not only streamlines
development but also positions your SDK as a professional tool that developers can confidently use in production.