Automatically generating Typescript API clients on build with NSwag
TL;DR
Want to know how you can generate and compile up to date Typescript API clients each time you build your solution? Take a look at this example on GitHub.
Want to generate a C# client, check out this post
Background
When you create an API using aspnetcore it is very easy to add a Swagger endpoint and SwaggerUI for exploring and testing your API. Once you have Swagger enabled you can also use the information Swagger exposes about your API to generate clients for the enpoints and operations your aspnet controllers expose.
With NSwag you can generate client code without having your API running, many generators require the swagger.json
file that is exposed when you run your API but NSwag doesn’t.
In this blogpost I will show you how to configure Swagger an NSwag so that up to date API clients are generated and compiled each time you build your solution. These clients can be packaged and published through NuGet for easy access to your API’s.
Configure Swagger and SwaggerUI with NSwag
First add the NSwag.AspNetCore
NuGet package to your API project:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Example.Api</AssemblyName>
<RootNamespace>Example.Api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.9" />
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7" />
<PackageReference Include="NSwag.MSBuild" Version="14.0.7">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
Next add the following code to your Program.cs
:
// .....
builder.Services.AddControllers().AddNewtonsoftJson();
builder.Services.AddOpenApiDocument(settings =>
{
settings.PostProcess = document =>
{
document.Info.Version = "v1";
document.Info.Title = "Example API";
document.Info.Description = "REST API for example.";
};
});
//.....
if(!app.Environment.IsProduction()) {
app.UseOpenApi();
app.UseSwaggerUi();
}
//......
This is enough for a basic Swagger configuration, if you run your aspnetcore API project and navigate to http://<host>:<port>/swagger
you will see SwaggerUI. This will also expose a swagger.json
document at http://<host>:<port>/swagger/v1/swagger.json
describing your API.
For more elaborate examples or explanation on how to configure NSwag have a look at the documentation for configuring the aspnetcore middleware.
Generate API clients with NSwag
Next setup a separate Clients
project (or whatever you want to name it) and add the NSwag.MSBuild
NuGet packages to your API project. We will use this package to generate the code for our API clients after the project is build, this way we regenerate our client code every time you build your project.
There are 2 things you need to add to your API project file to configure this:
- a
PackageReference
toNSwag.MSBuild
inside aItemGroup
- a custom Target that runs after the
Build
target with aCondition
. This target will invokenswag.exe
using annswag.json
config file to generate the required code.
Your project file has to look something like this:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Example.Api</AssemblyName>
<RootNamespace>Example.Api</RootNamespace>
</PropertyGroup>
.....
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.9" />
<PackageReference Include="NSwag.AspNetCore" Version="13.8.2" />
<PackageReference Include="NSwag.MSBuild" Version="13.8.2">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<Target Name="NSwag" BeforeTargets="AfterBuild" Condition="'$(TF_BUILD)'!='True'">
<Exec
ConsoleToMSBuild="true"
ContinueOnError="true"
Command="$(NSwagExe_Net80) run nswag.json /variables:...">
<Output TaskParameter="ExitCode" PropertyName="NSwagExitCode"/>
<Output TaskParameter="ConsoleOutput" PropertyName="NSwagOutput" />
</Exec>
<Message Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' == '0'" Importance="low"/>
<Error Text="$(NSwagOutput)" Condition="'$(NSwagExitCode)' != '0'"/>
</Target>
</Project>
Note that the condition Condition="'$(TF_BUILD)'!='True'"
is specific to Azure DevOps. This condition ensures that the client code not regenerated. I use this in my Azure DevOps builds so that the version of the code that is in source control is used instead of being regenerated. You can use any environment variable while running MSBuild. So if you are using a different CI system which sets a different environment variable you can use that instead. Else you can introduce your own MSBuild variable in the PropertyGroup
and pass that to dotnet.exe
using /p:MyVar=False
The NSwag
target is configured to log the output of nswag.exe
as MSBuild messages. If the exit code is anything but 0 and Error
message will be logs. It took me a while to figure this out but this way you can see in your MSBuild output why nswag.exe
is failing.
The easiest way to create a nswag.json
config file is by using NSwagStudio which you can install on Windows using an MSI
you can find here or you can take the nswag.json
file from my example repository on github and make modifications in that.
Below are the most important properties for this example (get the full nswag.json
file here):
{
"runtime": "Net80",
"documentGenerator": {
"aspNetCoreToOpenApi": {
...
"aspNetCoreEnvironment": "CodeGeneration",
...
}
},
"codeGenerators": {
"openApiToTypeScriptClient": {
...
"template": "Angular", //generate an Angular specific client
"promiseType": "Promise", //use Promises
"httpClass": "HttpClient", //use the Angular HttpClient
...
"injectionTokenType": "InjectionToken", //use InjectionToken to configure the baseURL
"rxJsVersion": 6.0,
"dateTimeType": "OffsetMomentJS", //use MomentJS for timestamps
...
"output": "$(TypescriptOutputPath)/api.generated.clients.ts"
}
}
}
The documentGenerator.aspNetCoreToOpenApi.aspNetCoreEnvironment
is especially important because ASP.net core uses Production
as the default environment. If you something like this in your code:
if(!app.Environment.IsProduction()) {
app.UseOpenApi();
app.UseSwaggerUi();
}
Then the OpenAPI and Swagger endpoints are only available when the environment is set to something other than Production
.
When the clients are generated you still have to add a way for them to authenticate. In Angular you can do this with an HttpInterceptor:
...
@Injectable()
export class TokenInterceptor {
/**
* Creates an instance of TokenInterceptor.
* @memberof TokenInterceptor
*/
constructor() {}
/**
* Intercept all HTTP request to add JWT token to Headers
* @param {HttpRequest<any>} request
* @param {HttpHandler} next
* @returns {Observable<HttpEvent<any>>}
* @memberof TokenInterceptor
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.debug('appending bearer token to request:', request)
request = request.clone({
setHeaders: {
Authorization: `Bearer ${"<get your access token here"}`
}
});
return next.handle(request);
}
}
Now build the Api
project to generate the clients and contracts and the build your Client
project. All that is left to do is to package your Client
project as a NuGet package and share it with the users of your API. Having the Client
project in the same solution as your aspnetcore API allows you to automatically build and publish up to date clients for your API.
A fully working example is available on GitHub. If you encounter issues with this example create an issue on that repository or leave a comment here.
Want to generate a C# client, check out this post
NOTE 1: There is no need to put the generated Typescript code into a .csproj
. I merely did that here so that both the C# and Typescript examples look similar. If you, for example, serve an Angular app from your ASP.Net Core application you can can generate the Typescript client inside the angular project instead so that you have fully typed access to your API.
NOTE 2: I used to do this the other way around, meaning that the Client
project contained the NSwag MSBuild target. This caused quite some build errors when concurrently building because the Client
project had an implicit dependency on the Api
project. So I decided to swap it around and make the Api
project generate the code into the Client
project instead.
Theoretically the generated code can get out of sync when the Client
project builds before the Api
project. But, to generated a changed client you will always have to modify the Api
project first to add or modify some Controller
. So as soon as you compile those changes the Client
project will be updated. In short, this should never happen in practice. There is an MSBuild “trick” to ensure the correct build order without adding an assembly reference to the Api
project but as said, you shouldn’t need that. *
Credits
Thanks to my colleague Erick Segaar for providing the updated code for ASP.net core 8 and NSwag 14.
Cover photo by darylgio agoncillo
Updates
- 21/04/2024: upgraded to ASP.net core 8, NSwag 14, and added a note about the
aspNetCoreEnvironment
property innswag.json
.