When you opened up this article to read it, your phone or computer sent several HTTP requests to the place where it was hosted. Also, various APIs were sent pieces of data related to user analytics and the like.
Pretty much every app today will invoke some sort of API, which is a way for apps and websites to send data around.
For APIs, you are responsible for writing what happens on the server, how the database is queried, etc. Over time, apps increase in complexity, so our API requests become more complex — and so do our responses.
Our client-side app needs updates to how these API are invoked. Thus, you are responsible for “balancing the equation” by making all the same updates to the client app. This can take a lot of time in itself. Worse still, you’re susceptible to making a typo or something in this code, and can be stuck wondering why nothing is working.
Wouldn’t it be nice if we could remove ourselves from making a typo in this code, and even from spending hours writing this API invocation?
Boom, that’s what OpenAPI-generated clients are for.
In the earlier days of the web, it wasn’t hard to write your own code to invoke an HTTP client, retrieve some data (or post some), and deal with the result. And, for new projects, it can be tempting to roll your own API service that essentially does just this.
However, apps are much more complicated these days. As we add more functionality to our app, our API surface swells as well. Whenever we adjust some code in our web app or add a new HTTP action, we also have to add the same functionality within our API client. This endless “balancing of the equation” takes a lot of time.
Things continue to increase in complexity as we introduce Bearer tokens for authentication. These tokens aren’t set-and-forget, though; they have a short lifespan to ensure application security. When they expire, they get sent to our identity service with a refresh token, and a new Bearer token is returned. It’s not realistic to expect our user to have a failed request that will work again in a moment, so we have to retry the failed operation on the fly.
All of this complexity adds time, and there’s also a chance we could make a mistake. Worse, it’s possible that we could introduce a hard-to-troubleshoot problem in how we invoke our server API, caused by something as simple as a typo in our API client that we wrote.
The other problem is that, once you have an API that exposes certain actions, you may want to invoke it, so you can test it and see what the response will be. If you’re particularly enterprising, you can do this yourself using terminal commands like cURL, or put together some kind of script to do it for you. But still, this is more code that is lovingly crafted by yourself, and needs to be maintained.
In more recent years, other solutions like Postman have come on the scene to try to give you a one-stop shop for API usage and testing. But, again, having to maintain how Postman interacts with your API is quite a lot of work in itself.
When we think about documentation, we imagine sitting down to write something that describes how our application works. We associate a level of effort with doing that.
But when writing web apps, we mostly use HTTP to communicate back to our API’s. In doing so, we know the HTTP method (such as GET
/PATCH
/POST
etc). We also know the arguments that are being sent to these HTTP actions.
These are defined at compile-time. It’s not impossible for something to inspect these actions and produce some documentation on what the actions are called, and what sort of arguments they produce. With a bit more finesse, we can describe what kind of return codes we expect from the API. Finally, it’s possible to specify which actions require authentication and are protected, and we can even describe polymorphic data in the documentation.
There is a bit of a setup cost, and sometimes it takes some nudging for the documentation to work correctly. But the payoff is immense; you can generate ready-made API clients for your app in dozens of different languages and frameworks.
All you have to do is decorate your API with some OpenAPI bits and pieces. It doesn’t matter if someone wants a C#, PowerShell or Bash script client; they can just generate what they want.
To demonstrate, let’s create a simple C# Web API app that has two actions, an HTTP GET
and HTTP POST
:
app.MapGet("/catbreeds", () => { var catBreeds = new[] { "Persian", "Siamese", "Maine Coon", "British Shorthair", "Bengal", "Ragdoll", "Sphynx", "Russian Blue", "Scottish Fold", "American Shorthair" }; return catBreeds[Random.Shared.Next(catBreeds.Length)]; }); app.MapPost("/reverser", (string payload) => { var reversed = new string(payload.Reverse().ToArray()); return reversed; });
They’re two very simple actions, but they tell us something important:
GET
/POST
)string payload
in the second action)Then, early in our Program.cs
and pipeline setup, we configure OpenAPI to create the document:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { // Enable middleware to serve generated Swagger as a JSON endpoint. app.MapOpenApi(); }
Finally, we configure Swagger UI so we can interact with our API’s from a handy UI:
app.UseSwaggerUI(x => { x.SwaggerEndpoint("/openapi/v1.json", "v1"); });
And, with just that small amount of configuration, this is what happens when we go to our swagger/index.html
address within our browser:
Our actions are given an appropriate name, and we can see that they have the GET
and POST
verbs appropriately. Expanding an action describes what it does:
What I find most useful is the Try it out button.
Executing an API action from within the browser directly saves a lot of time and lets you test out what your API does before writing a client-side app that uses the API. In this case, once we hit Execute, we can see that our app invokes the API and produces a result:
It’s the same thing with the reverser action, accepting a query string parameter of payload for our string:
Most apps use some form of authentication, and OpenAPI/SwaggerUI let us invoke secured APIs in an accurate way. Let’s go ahead and add a (very basic) authentication safeguard for our app:
// Simple auth middleware app.Use(async (context, next) => { var path = context.Request.Path.Value?.ToLower(); // Skip auth for Swagger/OpenAPI endpoints if (path != null && (path.StartsWith("/swagger") || path.StartsWith("/openapi"))) { await next(); return; } var authHeader = context.Request.Headers.Authorization.ToString(); if (string.IsNullOrEmpty(authHeader) || authHeader != "Bearer fake_bearer_auth") { context.Response.StatusCode = 401; await context.Response.WriteAsync("Unauthorized"); return; } await next(); });
We always want our API documentation to be available, but we want to lock the rest of our API behind a Bearer token. Your implementation will differ, but in our case, it’s enough to just use the simple fake_bearer_auth
token.
Our AddOpenApi
call now becomes:
builder.Services.AddOpenApi(options => { options.AddDocumentTransformer((document, context, cancellationToken) => { document.Components ??= new OpenApiComponents(); document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, Scheme = "bearer", Description = "Enter 'fake_bearer_auth'" }; document.SecurityRequirements.Add(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty<string>() } }); return Task.CompletedTask; }); });
Swagger UI reads this configuration, and now our API actions have a lock icon next to them. An Authorize button becomes visible:
Our description is available, namely, telling us to enter fake_bearer_auth:
And now, we can see that fake_bearer_auth is included in the Authorization header, and our secured API responds appropriately! 🎉:
All of this up until now has been fine. But the real power of this approach comes with its ability to produce clients for pretty much any language or framework we can think of.
Seriously, there’s a lot:
Admittedly, the OpenAPI generator GitHub does come across a bit cluttered, and it’s hard to work out just how to use it.
npm init
in the shared directory to get a package.json
npm i @openapitools/openapi-generator-cli --save
in this directorymakeapi
option in the scripts
entityThis is going to look weird. It’s quite lengthy, but there’s a method to my madness. Here’s what mine looks like:
"makeapi": "export JAVA_OPTS=\"-Dio.swagger.parser.util.RemoteUrl.trustAll=true -Dio.swagger.v3.parser.util.RemoteUrl.trustAll=true\" && export NODE_TLS_REJECT_UNAUTHORIZED=0 && openapi-generator-cli generate -i https://localhost:7067/swagger/v1/swagger.json -g typescript-angular -o ./apiclient/client -p npmName=apiclient -p apiModulePrefix=ApiClient --enable-post-process-file --skip-validate-spec"
Obviously, this is a huge command to run and does some scary things (like tanking SSL verification). Naturally, that’s a bad idea. So why do we do it?
Typically, in development, we use a lot of self-signed certificates. Because it’s a Java app, the OpenAPI generator struggles with this; Java maintains its own repository of trusted certificates. Frequently, API generation would choke because of this private certificate problem.
SSL certificates exist to help ensure we’re talking to who we think we are talking to. They’re a cornerstone of modern-day internet security. Do we introduce security risks by not checking them when talking to localhost? Personally, I don’t think so.
You may feel differently. If you do, feel free to configure your self-signed certificates as you see fit.
The other commands are:
-g typescript-angular
— Create a Typescript API client for use in Angular-o ./apiclient/client
— Where to put the generated client-p npmName=apiclient
— What name the generated package should have-p apiModulePrefix=ApiClient
— What the generated API clients’ invocations should be prefixed with. Handy to know for sure that a certain class or interface is definitely generated and not coming from your own app--enable-post-process-file
— Format the generated files to make them more readable--skip-validate-spec
— The OpenAPI specification can be quite strict. Normally, API client generation will fail if your API isn’t conformant. This lets the generator make some concessions to ensure client generation while not technically adhering to the OpenAPI specificationNow, we can just run npm i
and then npm run makeapi
. We should receive our generated API client!
Within our Angular project, our component code looks like this:
cat = signal('unknown...'); reversedString = signal(''); // Obtain a reference to our genered ApiClientsService apiService = inject(ApiClientsService); textReverser: string = ""; async getCat(){ // And just call our API actions as we need to let catBreed = await firstValueFrom(this.apiService.apiCatbreedsGet()); // Deal with the result appropriately this.cat.set(catBreed); } async reverseText(){ let reversed = await firstValueFrom(this.apiService.apiReverserPost(this.textReverser)); this.reversedString.set(reversed); }
As you can see, we obtain a reference to the service and then call our API actions. The result type and signature are all inferred from our OpenAPI documentation:
And that’s it, easy peasy lemon squeezy.
Writing your own API clients, apart from being a gigantic waste of time, also means that you are responsible for how specific functionality works. For example, even injecting Bearer authentication can be a bit of a hazard if you’re not entirely sure what you’re doing.
When you generate your clients, you essentially generate a contract that states how your app will communicate with the API. Because it’s generated, all of that is done for you. The other details, like what tokens and authentication to use, can be implemented by yourself.
In Angular’s case, that means implementing an HTTP Interceptor that intercepts requests to the /api/ path and injects our fake bearer auth token. Our API invocation code is neatly in its own place; how our tokens get into the request is neatly in another area. It all makes for a nice separation of concerns.
Starting out with a generated API client means that you don’t have to throw out your hand-written one to move to OpenAPI/Swagger in the future. It also means that you’ll save a lot of time by not writing your own API client. Making that shift later will require a bit of grind, so doing it early in your project will help you in the long run.
Don’t forget to bring in the sample project from here. Tell us how you automate API generation in your projects and the benefits you’ve found in the comments below.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDiscover how the Interface Segregation Principle (ISP) keeps your code lean, modular, and maintainable using real-world analogies and practical examples.
<selectedcontent>
element improves dropdowns
is an experimental HTML element that gives developers control over how a selected option is displayed, using just HTML and CSS.
Learn how to implement an advanced caching layer in a Node.js app using Valkey, a high-performance, Redis-compatible in-memory datastore.
Learn how to properly handle rejected promises in TypeScript using Angular, with tips for retry logic, typed results, and avoiding unhandled exceptions.