Many of you have probably used OpenAPI in your projects, or at least heard of it. But from my regular conversations with developers, I’ve noticed something surprising: a lot of frontend developers still don’t fully take advantage of it.
This article is about how using OpenAPI has helped us speed up development, improve code consistency, and eliminate entire categories of errors. I'll also walk you through how to set it up and integrate it into a TypeScript project.
🧩 What Is OpenAPI, really?
At its core, OpenAPI is a tool that takes your Swagger definitions and generates a full API client, including:
- A wrapper for all your endpoints
- Typed models and DTOs (based on Swagger schema)
- Methods that handle requests for you (like
axios
orfetch
) with built-in type safety
In short: no more manually writing interfaces and HTTP calls. You get a fully typed, ready-to-use client that’s always in sync with the backend.
💡 Why OpenAPI matters
Here are some of the biggest benefits we’ve seen:
- Type safety out of the box: All request and response models are generated from Swagger. You no longer need to manually define interfaces or worry about mismatches.
- No more manual fetch/axios calls: All endpoints come with pre-written request methods.
- Consistency between backend and frontend: When something changes on the backend (e.g., a route is renamed or a payload changes), you just re-generate the schema and catch issues during compile time.
- Zero maintenance: Once configured, it’s basically plug-and-play — just update the schema when needed.
🛠 Setting up OpenAPI in your project
To get started, install the OpenAPI generator:
yarn add @openapitools/openapi-generator-cli
Create a config file openapi/apiConfig.json
with the following:
{
"prefixParameterInterfaces": true,
"supportsES6": true,
"typescriptThreePlus": true
}
Now, add scripts to your package.json
:
"scripts": {
"openapi": "yarn openapi:download && yarn openapi:generate",
"openapi:download": "curl https://yourserver.com/api/docs-json > ./openapi/openapi.json",
"openapi:generate": "openapi-generator-cli validate -i ./openapi/openapi.json && rm -rf src/openapi/api && openapi-generator-cli generate -i ./openapi/openapi.json -c ./openapi/apiConfig.json -g typescript-fetch -o src/openapi/api"
}
⚠️ Note: You may need to install JRE (Java Runtime Environment) for the generator to work.
📦 Building the API Client
After running yarn openapi
, you’ll get a fully generated src/openapi
folder with models, APIs, and types.
To simplify usage, let’s build a centralized API client:
import {
BonusApi,
ChatApi,
Configuration,
ServiceApi,
UserAccessApi,
UserInfoApi,
UsersApi,
UserTasksApi,
UserUtilsApi,
} from './api';
const configuration = new Configuration({
accessToken() {
return 'ACCESS_TOKEN';
},
get headers() {
return {
'x-language': 'en',
};
},
});
export const ApiClient = {
chat: new ChatApi(configuration),
service: new ServiceApi(configuration),
user: {
access: new UserAccessApi(configuration),
info: new UserInfoApi(configuration),
tasks: new UserTasksApi(configuration),
utils: new UserUtilsApi(configuration),
},
users: new UsersApi(configuration),
};
Now calling an API is as simple as:
const { accessToken } = await ApiClient.user.access.login({
appLoginPayloadDto: payload,
});
🔍 What’s under the hood?
Let’s take a look at the actual implementation of the login
method:
async loginRaw(
requestParameters: UserAccessApiLoginRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction
): Promise<runtime.ApiResponse<AppAccessUserTokenDto>> {
if (requestParameters['appLoginPayloadDto'] == null) {
throw new runtime.RequiredError('appLoginPayloadDto', 'Required parameter is missing.');
}
const headerParameters: runtime.HTTPHeaders = {
'Content-Type': 'application/json',
};
const response = await this.request({
path: `/app/user/access/login`,
method: 'POST',
headers: headerParameters,
body: AppLoginPayloadDtoToJSON(requestParameters['appLoginPayloadDto']),
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => AppAccessUserTokenDtoFromJSON(jsonValue));
}
async login(
requestParameters: UserAccessApiLoginRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction
): Promise<AppAccessUserTokenDto> {
const response = await this.loginRaw(requestParameters, initOverrides);
return await response.value();
}
You can see that everything is strictly typed — parameters, return values, and even runtime checks are in place.
⚠️ Limitations (for now)
There are still some things OpenAPI doesn’t support out of the box, such as:
- WebSockets
- Some custom middleware logic
These require custom solutions for now. But the OpenAPI ecosystem is rapidly evolving, and we expect more features to be supported over time.
See you in the comments — and thanks for reading! 🚀
Nice, makes me want to go clean up my own mess of HTTP calls tbh.