Here's a thing that happens all the time: you're working with someone on an app, they're handling most of the backend/API side, and you're doing the front-end. They ping you to say they've updated their API and you need to update your app to incorporate their changes.
If this were a 90's infomercial, this is the part where the screen would freeze in black-and-white and you'd make an exaggerated "harrumph" face. The announcer says, "there must be a better way!"
Well, in true 90's infomercial fashion, this is when the music changes to a jaunty, upbeat tune and a smiling lady with a perfectly-coiffed 90's bob cheerfully demonstrates the better way.
It's me. I'm your perfectly-coiffed cheerful 90's lady. I'm here to show you how, through the magic of Open API, we can generate our API clients based on an API description, saving yourself the effort of manually updating your glue code every time the API changes.
In this post, I'm going to focus on getting things working in the back-end API. We're going to use .NET because that's my bread-and-butter, but the concepts, if not the specifics, will apply to your back-end technology too.
Swaggering your way to an Open API
OK, so what is Open API in the first place? Simply put, it's a standard JSON/YAML schema for describing RESTful API's. Everything you need to know about how to talk to an API is in there:
- the endpoints
- their URLs
- which HTTP methods they use
- request and response types
- including the error types you can expect
- other important details like what kind of authentication is required
In other words, these are all the things you'd need to get from your counterpart to implement an API, except in a format that tools can read and understand. Pretty slick, huh! There's a whole universe of generator tools that can take an Open API file and generate a compatible client or the stubs for a compatible service.
In fact, one common way of using Open API is to design the Open API file first as a kind of contract, then generate the server and client automatically. You can be (fairly) confident that they'll be able to talk to each other without really effort on your part.
This format used to be called "Swagger", and you'll still see lots of references to Swagger online. Lots of people still call it that. In the most techincal sense they're talking about an older version of Open API, but you can generally think of "Swagger" and "Open API" as synonyms.
A quick note about Swashbuckle
Swashbuckle is the most commonly-used .NET library for doing Open API generation. Unfortunately, development seems to have dried up on it. Microsoft has announced that .NET 9 will ship with a first-party replacement for Swashbuckle, and that they'll remove it from the default ASP.NET Core template.
How it all fits together
High level, here's how we're going to make this work:
- Our ASP.NET Core backend will generate an Open API definition at build time.
- Our client app will pick up that Open API definition for its own build, and will generate a compatible client.
- There's no step 3!
Getting down to brass tacks, let's talk about the moving parts here.
On the server side:
- Swashbuckle.AspNetCore - this package provides us with an ASP.NET Core middleware that generates the Open API definition. Additionally, it includes a UI, which you've probably seen before, that lets us test out our API's in the browser.
- Microsoft.AspNetCore.Mvc.ApiExplorer - don't let the "MVC" name fool you, this works for all ASP.NET Core types, including API Controllers and minimal APIs. The API Explorer is a service that provides an object model representation of your API. Swashbuckle walks this object tree to generate the Open API description.
- Microsoft.Extensions.ApiDescription.Server - this is a fascinating little package. It includes build targets for your project for generating Open API files at build time, as well as a binary for launching your app and invoking the Open API generation. Crucially, it does not provide the actual generation machinery itself. That's delegated to your Open API library of choices. In our case, that's Swashbuckle.
Wait, what? Say that last part again.
OK, so here's how this works. When your project builds, the ApiDescription package adds a build target, GenerateOpenApiDocuments
, to your build pipeline. So far, so good, right? That build target checks to see if certain build properties are defined. If they are, then it will use those build properties to invoke an included tool, dotnet-getdocument.dll
. Here's the relevant MSBuild XML:
<_Command>dotnet "$(MSBuildThisFileDirectory)../tools/dotnet-getdocument.dll" --assembly "$(TargetPath)"
<_Command>$(_Command) --file-list "$(_OpenApiDocumentsCache)" --framework "$(TargetFrameworkMoniker)"
<_Command>$(_Command) --output "$(OpenApiDocumentsDirectory.TrimEnd('\'))" --project "$(MSBuildProjectName)"
<_Command Condition=" '$(ProjectAssetsFile)' != '' ">$(_Command) --assets-file "$(ProjectAssetsFile)"
<_Command Condition=" '$(PlatformTarget)' != '' ">$(_Command) --platform "$(PlatformTarget)"
<_Command Condition=" '$(PlatformTarget)' == '' AND '$(Platform)' != '' ">$(_Command) --platform "$(Platform)"
<_Command Condition=" '$(RuntimeIdentifier)' != '' ">$(_Command) --runtime "$(RuntimeIdentifier) --self-contained"
<_Command>$(_Command) $(OpenApiGenerateDocumentsOptions)
So, you might ask, what does dotnet-getdocument.dll
do? Skipping over a lot of infrastructure around the tool itself, the nut of it is here. In short, it loads your app's assembly, replaces some of the default services with no-op implementations to prevent the server from actually running, and then runs it.
To boil it down even further, it loads your app's assembly, runs the Program.Main
method, and waits for the app to finish starting. Once that's done, it can grab the IServiceProvider
for your app and look for an implementation of IDocumentProvider
. This is a simple little interface that compatible Open API implementations (like Swashbuckle) can implement to provide a list of Open API documents to this tool.
From there, it's a simple matter of getting a list of documents from the provider, invoking the document generation on each, and saving the result to a file in the obj
directory.
Pretty slick, right? In fact, this isn't the only tool that uses a trick like this to run operations against your fully-configured app. Another common one is the dotnet ef
tool, which does largely the same thing to generate migrations.
Less talk, more rock
OK, that's a ton of preamble and no code. Let's make some stuff.
For starters, let's just make a standard ASP.NET Core API template. Literally, all I've done here is set up a very simple little Web API project. I like to do this from the command line, but if you're following along from home, the Visual Studio template will work just fine. Either way, When you're done you should have the template Weather Forecast API with a Swagger page:
Now let's do some cool stuff.
First, let's add the packages we want:
dotnet add package Microsoft.Extensions.ApiDescription.Server
And then let's build and see what happens:
dotnet build
Whoa, what's this?
Let's check out the JSON file it generated and see what we got:
You can pretty much figure out how this file works just from the structure. We've got some endpoints, their HTTP verbs, and some types. The schemas for those types are defined below in the components
key.
Why this is cool
Now, this is literally, byte-for-byte, the same file you'd get if you ran your project and followed the swagger.json
link in the Swagger UI. So what's the point of doing this at build time?
In the next couple of posts, I'm going to show you two cool ways of using this to auto-generate a client. We'll make one for .NET, and another for TypeScript. Stay tuned! Same Bat-time, same Bat-channel.