Generete type for Frontend from OpenApi/Swagger
I already have a Data Transfer Object (DTO) defined on my backend and have integrated OpenApi/Swagger. Still, I am required to write data types on the frontend — a task that can be redundant and potentially error-prone. Thus, I have been exploring methods to generate these data types directly from OpenApi/Swagger to streamline the development process.
TL;DR
I utilize openapi-typescript to generate types from Swagger and then create a fetch function using these types. See the last section for the function code.
TypeScript types
First, I need some methods to convert Swagger into TypeScript. So, I utilize openapi-typescript, which facilitates the generation of TypeScript types from fixed OpenAPI schemas.
You can install it along with TypeScript, as a development dependency like so
npm i -D openapi-typescript typescript
Next, add a script to your package.json file to facilitate easy generation of TypeScript interfaces:
"scripts": {
"gen:interface": "openapi-typescript https://backend:3000/swagger-json -o ./interfaces/interface.ts"
}
Right now you can use type from your backend
import { components } from "@/interface/interface"
export type OrderListDto = components["schemas"]["OrderListDto"]
Utilize more
Taking into consideration the types we have, we can develop a function that facilitates auto-suggestion, as well as ensuring type safety in auto request and response.
You can jump to the last part for the completed function.
// start
function fetchToBackend(url: string, options: any) {
}
Enhance URL Auto Suggestion
//The below generated type ./interfaces/interface.ts (partial)
export interface paths {
"/user/update-profile": {
post: operations["UserController_updateProfile"]
}
"/user/upload-avatar": {
get: operations["UserController_getUploadAvatarPresignedUrl"]
}
"/user/{username}": {
get: operations["UserController_getPublicUserInfo"]
}
}
Here, it’s clear that our ‘paths’ interface represents each accessible route, we add a generic type from ‘paths’ and employ it on the URL parameter.
import { paths } from "./interface/interface"
async function fetchToBackend<K extends keyof paths>(url: K, options: any) {
}
so we get URL suggestion from that
Enforce request body type safe
//The below generated type ./interfaces/interface.ts (partial)
export interface paths {
"/user/update-profile": {
post: operations["UserController_updateProfile"]
}
}
export interface operations {
UserController_updateProfile: {
requestBody: {
content: {
"application/json": components["schemas"]["UpdateUserDto"]
}
}
responses: {
201: {
content: never
}
}
}
}
export interface components {
schemas: {
UpdateUserDto: {
displayName?: string
bio?: string
}
}
}
So we need UpdateUserDto for our request body
//This will extract type for DTO and allow any key for flexibility
type CustomRequestInit<K extends keyof paths, M extends keyof paths[K]> = Omit<RequestInit, "body"> & {
body?: Extract<
paths[K][M],
{ requestBody: { content: { "application/json": Record<string, any> } } }
>["requestBody"]["content"]["application/json"] & { [key: string]: any }
}
//If you want to allow on key in DTO use this
//type CustomRequestInit<K extends keyof paths, M extends keyof paths[K]> = Omit<RequestInit, "body"> & {
// body?: Extract<
// paths[K][M],
// { requestBody: { content: { "application/json": Record<string, any> } } }
// >["requestBody"]["content"]["application/json"]
//}
async function fetchToBackend<K extends keyof paths, M extends keyof paths[K]>(
url: K,
options: CustomRequestInit<K, M>
& { method: M })
{
}
const resp = fetchToBackend("/user/update-profile",{body:{}})
this custom type will extract the DTO that we need
Add support for URL params
type ExtractParams<K extends string> = K extends `${infer L}{${infer P}}${infer R}` ? P | ExtractParams<R> : never
async function fetchToBackend<K extends keyof paths, M extends keyof paths[K]>(
url: K,
options: CustomRequestInit<K, M>
& { method: M }
& ([ExtractParams<K>] extends [never]? {} : { params: Record<ExtractParams<K>, any> }))
{
}
now we have type force when URL require params
Add argument for any URL query
// add querys?: Record<string, any>
async function fetchToBackend<K extends keyof paths, M extends keyof paths[K]>(
url: K,
options: CustomRequestInit<K, M>
& { method: M; querys?: Record<string, any> }
& ([ExtractParams<K>] extends [never] ? {} : { params: Record<ExtractParams<K>, any> })) {
}
const resp = fetchToBackend("/user/{username}", { method: "get", params: { username: "Test" }, querys: { test: "test" } })
Enhance response type
async function fetchToBackend<K extends keyof paths, M extends keyof paths[K]>(
url: K,
options: CustomRequestInit<K, M>
& { method: M; querys?: Record<string, any> }
& ([ExtractParams<K>] extends [never] ? {} : { params: Record<ExtractParams<K>, any> })
): Promise<Extract<
paths[K][M],
{ responses: { 200: { content: { "application/json": Record<string, any> } } } }
>["responses"][200]["content"]["application/json"] extends never
? { [key: string]: any }
: Extract<
paths[K][M],
{ responses: { 200: { content: { "application/json": Record<string, any> } } } }
>["responses"][200]["content"]["application/json"] & { [key: string]: any }>
{
return {} as any
}
We arrived at this solution to extract specific types from the interface. However, we believe there’s still room for improvement. We appreciate any insights or suggestions for enhancing this section of our coding strategy.
Completed function
async function fetchToBackend<K extends keyof paths, M extends keyof paths[K]>(
url: K,
options: CustomRequestInit<K, M>
& { method: M; querys?: Record<string, any> }
& ([ExtractParams<K>] extends [never] ? {} : { params: Record<ExtractParams<K>, any> })
): Promise<Extract<
paths[K][M],
{ responses: { 200: { content: { "application/json": Record<string, any> } } } }
>["responses"][200]["content"]["application/json"] extends never
? { [key: string]: any }
: Extract<
paths[K][M],
{ responses: { 200: { content: { "application/json": Record<string, any> } } } }
>["responses"][200]["content"]["application/json"] & { [key: string]: any }>
{
//add you login here
//Example
//1. construct options for request
//2. replace params on url
//3. add query params
//4. make request to backend
return {} as any
}
const resp = fetchToBackend("/user/{username}", { method: "get", params: { username: "Test" }, querys: { test: "test" } })