Enforcing contracts between projects implemented in different technologies is not a trivial task and there are 2 ways to tackle it down:
- automated contract tests (e.g. using Pact framework);
- auto-generated files on the consumer side, so contracts are enforced at the compilation time (usually, changes on the provider side trigger file generation on the consumer side).
The second option is way more tempting and implies writing less code, so let’s talk about it.
.NET Core +
TypeScript bundle with a WebAPI back-end and SPA front-end (e.g.
React). The task boils down to generating
TypeScript files (API clients) for the API controllers written in
C# (or any other .NET language). Of course, including all the dependencies and Data Transfer Objects (DTO).
The ideal solution would generate
TypeScript code for API controllers including dependencies and meeting the following criteria:
TypeScriptfiles locally either on saving the corresponding
C#files or on building the .NET project. Useful when one has ownership of the back-end and the front-end.
TypeScriptfiles in the CI/CD pipeline to re-enforce the contracts.
- Have one
TypeScriptfile/class per corresponding
C#file/class. It gives clear separation of models from API clients, simplifies tracing changes (who/when added ‘foo’ to the class?), debugging and other maintenance tasks.
- Support custom serialisation/de-serialisation for non-trivial data types (e.g. date values presented in JSON as
'YYYY-MM-DD'get converted to
- Ability to customise generation of API clients (e.g. specify the base class, add HTTP headers, etc.).
Needless to say that the tool has to generate decent code, handle inheritance, generic types, etc.
Though, several points are deliberately left behind as less important:
- Constraints on the data type of
TypeScriptDTOs. Of course, interfaces would be the preferable option as they have no
TypeScriptAPI clients that may backfire if someone uses a generated DTO outside of the API client.
- Ability to specify a subset of controllers and/or DTOs for processing. Let’s keep it in the ’nice to have’ bucket, as dealing with excessive data can be left for better times.
- And we all love GraphQL, but here we’re generating code from the .NET back-end, so bringing GraphQL and using tools like GraphQL Code Generator is out of the picture.
Microsoft recommends to use Swashbuckle.AspNetCore or NSwag when dealing with the OpenAPI specification and producing a Swagger specification file (
swagger.json). How to get TypeScript API clients from here?
No doubt, NSwag is the most popular package for the job. Its TypeScript client generator has good defaults and does a decent job with minimum tweaks (though the UI makes it easy to tweak). It natively supports Angular and others (React, Vue) via the browser built-in window.fetch and recently added Axios (though that one is still in preview and has some issues), also does AngularJS, JQuery and Aurelia. Check out the Microsoft docs as a starting point for NSwag.
Custom serialisation is there and it gives quite a lot:
- The DTOs can be generated as interfaces or classes with added methods for deserialization of complex types.
- Date/time fields are handled out-of-the-box (even supports MomentJS).
- Type Mappers will generate custom object JSON Schemas for your .NET types and it’s possible to specify custom JSON.parse functions.
Customising generated TypeScript API clients is also possible (e.g. custom transformation in the base class). Though tweaks are allowed in certain places only, but it will satisfy most of the needs.
NSwag is trying to do everything. It even has a middleware to generate swagger.json and serve Swagger UI (just install the NSwag.AspNetCore NuGet package). Who needs Swashbuckle.AspNetCore after that?..
And a cherry on top, the tool has a massive community, that means it’s well-tested and information is easily available. So, should you look no further?
Nope, NSwag doesn’t meet criteria #3, as everything gets generated in one super file with tons of boilerplate (here is the Wailing Wall for the multiple files feature)… And all the excitement comes to an abrupt halt. Though, there is a way to hack NSwag guts and save the generated content to separate files…
2. Swagger Codegen
Swagger-codegen is a Swiss Army knife for generating API client libraries for dozens of languages. When it comes to
TypeScript it supports the usual (Angular, AngularJS, Aurelia, Fetch API, and jQuery) plus NodeJS and InversifyJS options. Though, doesn’t have Axios.
Pleasant surprise – Swagger Codegen creates separate files for DTOs and API controllers, but does it only for the Angular and InversifyJS options. The later can be used in ReactJS/VueJS projects if you don’t mind a dependency on InversifyJS.
One might be put off by its installation options – need to bring a dependency on Java runtime or Docker (though it ticks the CI/CD requirement). Alternatively, one can also generate API clients using the online generators. That may do for smaller projects.
3. OpenAPI Generator
Talking about Swagger-codegen would be unfair not to mention its less famous fork. On May 2018, many contributors of Swagger Codegen decided to fork out to maintain a community-driven version called OpenAPI Generator (here is Q&A on the subject).
From the consumer point of view the two projects are pretty similar – need Docker or Java to run, has an online generator, supports dozens of languages. But OpenAPI Generator stands out in two ways:
- Has more client generators with more TypeScript formats adding a pure RxJs version, Axios and Redux Query options.
- Generates separate files for DTOs and API services for all the TypeScript options.
In addition, the OpenAPI Generator team appear to be more responsive on their GitHub Issues (e.g. see all TypeScript issues).
So it’s all nice and sweet, but I wish it had a better handling of complex DTOs.
Next one is AutoRest, a very modest tool from the MS Azure team that supports just 7 languages (yep, I’m completely spoiled by now). The core module is a NPM package written in NodeJS, but you need Autorest.TypeScript plugin to generate some TypeScript code (and the plugin needs .NET Core 2 runtime). Overall, it gives a good integration with the development environment and the CI/CD pipeline.
That’s it with good news. It doesn’t generate separate TypeScript files and has very minimal customisation of API clients.
But the biggest disappointment is the quality of the generated TypeScript code. It is at least questionable. The plugin doesn’t generate framework specific code, produces class mappers instead of DTOs and brings a dependency on
@azure/ms-rest-js that contains all the logic for handling XHR requests, mappers, etc…
It seems that the main purpose of AutoRest is to provide a painless way for packing up API clients in NPM packages. That is quite an unusual use-case.
Another option would be WebApiClientGen (aka Strongly Typed Client API Generators) that supports Angular, Axios, Fetch API, Aurelia and jQuery.
The project keeps up with new versions of .NET, Angular, etc., has some articles and good support on GitHub. But what spooked me is a quite invasive approach to generate files (check out this article). Essentially, a bundle of NuGet packages adds an API end-point(!) that consumes a
json file with parameters and outputs generated files. My lack of trust to NuGet packages was seeded by Phil Haack 7 years ago and after the event-stream issue in November 2018 my anxiousness kicks in when a package adds middleware or end-points.
In regard to generated TypeScript code, it all lands in one file. Customisation options are quite limited, but better than in AutoRest. Other differences from Swagger tools are described in this author’s post.
And last but not least, a tool that will blow you away – TypeWriter. It is an extension for Visual Studio that generates TypeScript files from C# code files using TypeScript Templates. It may sound scary, but check out these examples.
Yes, adoption of this method is not easy. You need:
- Conquer the learning curve (half a day).
- Write your templates for DTOs and controllers (1-3 days, depending on the specifics).
- Enforce that devs writing the back-end code have TypeWriter installed in their Visual Studio / JetBrains Rider / VS Code and the code gets changed only through the IDE.
But once you’ve done it, rolling out the solution on another project would be very quick.
As per the above requirements, the final solution would meet almost all of them. The TypeScript code gets generated on saving the C# files in the IDE. The generated files will reflect the folder structure of their C# counterparts. Deserialization of complex DTOs is completely under control (would recommend to engage ES6 decorators and class-transformer). Customisation of API services has no limits.
The cons are:
- Extra maintenance burden (as any code in your project). Things change (new versions of C#/TypeScript get released, devs leave the project), so the quality requirements to the template code should be the same as to the rest of the project and the knowledge shared.
- Hard to use it in the CI/CD pipeline, as the extension is tightly coupled with IDE and doesn’t have a CLI.
Need a tailored solution or just feel adventurous? Go with the TypeWriter option and roll up your sleeves.
DTO generators: Reinforced Typings and TypeGen
There are two more strong players for generating TypeScript DTOs, but… DTOs only, as they don’t handle API at all:
Both tools can be installed as NuGet packages and export DTOs marked with special attributes or configured in the Fluent way (via calling static methods on
ConfigurationBuilder builder in the
Startup.cs). TypeGen also provides a CLI tool.
They both generate separate files for DTOs, have decent support for complex data types and well-documented.
Definitely, they’re worth a try if you can put up with hand-crafted API clients. But for a lack of that essential functionality, I won’t include them in the total score.
Let’s put all the 6 options through the prism of the outlined requirements and give my very subjective score:
|1. Auto-generating on build/save||10||8||8||9||6||10|
|2. Auto-generating in CI/CD||10||8||10||9||6||-|
|3. DTOs/services in separate files||-||8||10||-||-||10|
|4. Deserialization of complex DTOs||8||-||-||-||-||10*|
|5. Customisation of API services||6||4||4||2||4||10*|
where * means that reaching the 10 is possible, but it’s on the devs shoulders.
Sadly enough, there is no ideal TypeScript generator and trade-offs to be made.