Generating SDKs¶
BecauseFastAPI is based on theOpenAPI specification, its APIs can be described in a standard format that many tools understand.
This makes it easy to generate up-to-datedocumentation, client libraries (SDKs) in multiple languages, andtesting orautomation workflows that stay in sync with your code.
In this guide, you'll learn how to generate aTypeScript SDK for your FastAPI backend.
Open Source SDK Generators¶
A versatile option is theOpenAPI Generator, which supportsmany programming languages and can generate SDKs from your OpenAPI specification.
ForTypeScript clients,Hey API is a purpose-built solution, providing an optimized experience for the TypeScript ecosystem.
You can discover more SDK generators onOpenAPI.Tools.
Tip
FastAPI automatically generatesOpenAPI 3.1 specifications, so any tool you use must support this version.
SDK Generators from FastAPI Sponsors¶
This section highlightsventure-backed andcompany-supported solutions from companies that sponsor FastAPI. These products provideadditional features andintegrations on top of high-quality generated SDKs.
By ✨sponsoring FastAPI ✨, these companies help ensure the framework and itsecosystem remain healthy andsustainable.
Their sponsorship also demonstrates a strong commitment to the FastAPIcommunity (you), showing that they care not only about offering agreat service but also about supporting arobust and thriving framework, FastAPI. 🙇
For example, you might want to try:
Some of these solutions may also be open source or offer free tiers, so you can try them without a financial commitment. Other commercial SDK generators are available and can be found online. 🤓
Create a TypeScript SDK¶
Let's start with a simple FastAPI application:
fromfastapiimportFastAPIfrompydanticimportBaseModelapp=FastAPI()classItem(BaseModel):name:strprice:floatclassResponseMessage(BaseModel):message:str@app.post("/items/",response_model=ResponseMessage)asyncdefcreate_item(item:Item):return{"message":"item received"}@app.get("/items/",response_model=list[Item])asyncdefget_items():return[{"name":"Plumbus","price":3},{"name":"Portal Gun","price":9001},]Notice that thepath operations define the models they use for request payload and response payload, using the modelsItem andResponseMessage.
API Docs¶
If you go to/docs, you will see that it has theschemas for the data to be sent in requests and received in responses:

You can see those schemas because they were declared with the models in the app.
That information is available in the app'sOpenAPI schema, and then shown in the API docs.
That same information from the models that is included in OpenAPI is what can be used togenerate the client code.
Hey API¶
Once we have a FastAPI app with the models, we can use Hey API to generate a TypeScript client. The fastest way to do that is via npx.
npx@hey-api/openapi-ts-ihttp://localhost:8000/openapi.json-osrc/clientThis will generate a TypeScript SDK in./src/client.
You can learn how toinstall@hey-api/openapi-ts and read about thegenerated output on their website.
Using the SDK¶
Now you can import and use the client code. It could look like this, notice that you get autocompletion for the methods:

You will also get autocompletion for the payload to send:

Tip
Notice the autocompletion forname andprice, that was defined in the FastAPI application, in theItem model.
You will have inline errors for the data that you send:

The response object will also have autocompletion:

FastAPI App with Tags¶
In many cases, your FastAPI app will be bigger, and you will probably use tags to separate different groups ofpath operations.
For example, you could have a section foritems and another section forusers, and they could be separated by tags:
fromfastapiimportFastAPIfrompydanticimportBaseModelapp=FastAPI()classItem(BaseModel):name:strprice:floatclassResponseMessage(BaseModel):message:strclassUser(BaseModel):username:stremail:str@app.post("/items/",response_model=ResponseMessage,tags=["items"])asyncdefcreate_item(item:Item):return{"message":"Item received"}@app.get("/items/",response_model=list[Item],tags=["items"])asyncdefget_items():return[{"name":"Plumbus","price":3},{"name":"Portal Gun","price":9001},]@app.post("/users/",response_model=ResponseMessage,tags=["users"])asyncdefcreate_user(user:User):return{"message":"User received"}Generate a TypeScript Client with Tags¶
If you generate a client for a FastAPI app using tags, it will normally also separate the client code based on the tags.
This way, you will be able to have things ordered and grouped correctly for the client code:

In this case, you have:
ItemsServiceUsersService
Client Method Names¶
Right now, the generated method names likecreateItemItemsPost don't look very clean:
ItemsService.createItemItemsPost({name:"Plumbus",price:5})...that's because the client generator uses the OpenAPI internaloperation ID for eachpath operation.
OpenAPI requires that each operation ID is unique across all thepath operations, so FastAPI uses thefunction name, thepath, and theHTTP method/operation to generate that operation ID, because that way it can make sure that the operation IDs are unique.
But I'll show you how to improve that next. 🤓
Custom Operation IDs and Better Method Names¶
You canmodify the way these operation IDs aregenerated to make them simpler and havesimpler method names in the clients.
In this case, you will have to ensure that each operation ID isunique in some other way.
For example, you could make sure that eachpath operation has a tag, and then generate the operation ID based on thetag and thepath operationname (the function name).
Custom Generate Unique ID Function¶
FastAPI uses aunique ID for eachpath operation, which is used for theoperation ID and also for the names of any needed custom models, for requests or responses.
You can customize that function. It takes anAPIRoute and outputs a string.
For example, here it is using the first tag (you will probably have only one tag) and thepath operation name (the function name).
You can then pass that custom function toFastAPI as thegenerate_unique_id_function parameter:
fromfastapiimportFastAPIfromfastapi.routingimportAPIRoutefrompydanticimportBaseModeldefcustom_generate_unique_id(route:APIRoute):returnf"{route.tags[0]}-{route.name}"app=FastAPI(generate_unique_id_function=custom_generate_unique_id)classItem(BaseModel):name:strprice:floatclassResponseMessage(BaseModel):message:strclassUser(BaseModel):username:stremail:str@app.post("/items/",response_model=ResponseMessage,tags=["items"])asyncdefcreate_item(item:Item):return{"message":"Item received"}@app.get("/items/",response_model=list[Item],tags=["items"])asyncdefget_items():return[{"name":"Plumbus","price":3},{"name":"Portal Gun","price":9001},]@app.post("/users/",response_model=ResponseMessage,tags=["users"])asyncdefcreate_user(user:User):return{"message":"User received"}Generate a TypeScript Client with Custom Operation IDs¶
Now, if you generate the client again, you will see that it has the improved method names:

As you see, the method names now have the tag and then the function name, now they don't include information from the URL path and the HTTP operation.
Preprocess the OpenAPI Specification for the Client Generator¶
The generated code still has someduplicated information.
We already know that this method is related to theitems because that word is in theItemsService (taken from the tag), but we still have the tag name prefixed in the method name too. 😕
We will probably still want to keep it for OpenAPI in general, as that will ensure that the operation IDs areunique.
But for the generated client, we couldmodify the OpenAPI operation IDs right before generating the clients, just to make those method names nicer andcleaner.
We could download the OpenAPI JSON to a fileopenapi.json and then we couldremove that prefixed tag with a script like this:
importjsonfrompathlibimportPathfile_path=Path("./openapi.json")openapi_content=json.loads(file_path.read_text())forpath_datainopenapi_content["paths"].values():foroperationinpath_data.values():tag=operation["tags"][0]operation_id=operation["operationId"]to_remove=f"{tag}-"new_operation_id=operation_id[len(to_remove):]operation["operationId"]=new_operation_idfile_path.write_text(json.dumps(openapi_content))import*asfsfrom'fs'asyncfunctionmodifyOpenAPIFile(filePath){try{constdata=awaitfs.promises.readFile(filePath)constopenapiContent=JSON.parse(data)constpaths=openapiContent.pathsfor(constpathKeyofObject.keys(paths)){constpathData=paths[pathKey]for(constmethodofObject.keys(pathData)){constoperation=pathData[method]if(operation.tags&&operation.tags.length>0){consttag=operation.tags[0]constoperationId=operation.operationIdconsttoRemove=`${tag}-`if(operationId.startsWith(toRemove)){constnewOperationId=operationId.substring(toRemove.length)operation.operationId=newOperationId}}}}awaitfs.promises.writeFile(filePath,JSON.stringify(openapiContent,null,2),)console.log('File successfully modified')}catch(err){console.error('Error:',err)}}constfilePath='./openapi.json'modifyOpenAPIFile(filePath)With that, the operation IDs would be renamed from things likeitems-get_items to justget_items, that way the client generator can generate simpler method names.
Generate a TypeScript Client with the Preprocessed OpenAPI¶
Since the end result is now in anopenapi.json file, you need to update your input location:
npx@hey-api/openapi-ts-i./openapi.json-osrc/clientAfter generating the new client, you would now haveclean method names, with all theautocompletion,inline errors, etc:

Benefits¶
When using the automatically generated clients, you would getautocompletion for:
- Methods.
- Request payloads in the body, query parameters, etc.
- Response payloads.
You would also haveinline errors for everything.
And whenever you update the backend code, andregenerate the frontend, it would have any newpath operations available as methods, the old ones removed, and any other change would be reflected on the generated code. 🤓
This also means that if something changed, it will bereflected on the client code automatically. And if youbuild the client, it will error out if you have anymismatch in the data used.
So, you woulddetect many errors very early in the development cycle instead of having to wait for the errors to show up to your final users in production and then trying to debug where the problem is. ✨







