NSwag generates strongly-typed TypeScript clients from an OpenAPI spec. Instead of Angular developers manually writing HTTP service classes with correct endpoint URLs, parameter types, and response types, NSwag generates them automatically. The generated client handles serialisation, authentication headers, and error handling consistently. When the API changes, regenerating the client catches type mismatches at compile time — a breaking API change immediately produces TypeScript compile errors rather than runtime failures.
NSwag TypeScript Client Generation
// dotnet add package NSwag.AspNetCore
// ── Program.cs — add NSwag ────────────────────────────────────────────────
builder.Services.AddOpenApiDocument(config =>
{
config.Title = "BlogApp API";
config.Version = "v1";
config.AddSecurity("Bearer", new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
});
config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("Bearer"));
});
if (app.Environment.IsDevelopment())
{
app.UseOpenApi(); // serves /swagger/v1/swagger.json
app.UseSwaggerUi(); // serves Swagger UI at /swagger
app.UseReDoc(); // serves ReDoc at /api-docs
}
// ── nswag.json — client generation configuration ──────────────────────────
// {
// "runtime": "Net80",
// "defaultVariables": null,
// "documentGenerator": {
// "aspNetCoreToOpenApi": {
// "project": "src/BlogApp.Api/BlogApp.Api.csproj",
// "output": "swagger.json"
// }
// },
// "codeGenerators": {
// "openApiToTypeScriptClient": {
// "generateClientClasses": true,
// "generateClientInterfaces": true,
// "template": "Angular",
// "httpClass": "HttpClient",
// "injectHttpClient": true,
// "baseUrlTokenName": "API_BASE_URL",
// "generateOptionalParameters": true,
// "output": "src/app/api/client.ts"
// }
// }
// }
// ── Generated TypeScript client (snippet) ────────────────────────────────
// export class PostsClient {
// constructor(
// @Inject(API_BASE_URL) private baseUrl: string,
// private http: HttpClient
// ) {}
//
// getById(id: number): Observable<PostDto> {
// return this.http.get<PostDto>(`${this.baseUrl}/api/v1/posts/${id}`)
// .pipe(map(response => response as PostDto));
// }
//
// create(request: CreatePostRequest): Observable<PostDto> {
// return this.http.post<PostDto>(`${this.baseUrl}/api/v1/posts`, request);
// }
// }
HttpClient and returns Observable<T> — the same pattern Angular services use. The generated models match the server-side DTOs exactly (camelCase property names from the server’s PropertyNamingPolicy.CamelCase configuration). When the server adds a field to PostDto, the generated TypeScript interface also gets that field after regeneration. This tight coupling between server DTOs and client models is the primary value of code generation.nswag run nswag.json && git diff --exit-code src/app/api/client.ts || (echo "Generated client is out of date" && exit 1).@Injectable() export class PostsService { constructor(private client: PostsClient) {} }. The wrapper handles cross-cutting concerns; the generated client handles the HTTP contract.Integrating the Generated Client in Angular
// ── Angular Module registration ────────────────────────────────────────────
// In app.module.ts (or environment-specific providers):
// providers: [
// PostsClient,
// UsersClient,
// { provide: API_BASE_URL, useValue: environment.apiBaseUrl },
// ]
// ── Using the generated client in an Angular component ────────────────────
// @Component({ ... })
// export class PostListComponent implements OnInit {
// posts: PostSummaryDto[] = [];
//
// constructor(private postsClient: PostsClient) {}
//
// ngOnInit(): void {
// this.postsClient.getAll(1, 10).subscribe({
// next: result => this.posts = result.items,
// error: err => console.error('Failed to load posts', err)
// });
// }
// }
Common Mistakes
Mistake 1 — Manually editing generated TypeScript client (edits lost on next generation)
❌ Wrong — custom error handling added to the generated client; overwritten on next nswag run.
✅ Correct — wrap the generated client in a service class; add custom logic in the wrapper.
Mistake 2 — Not regenerating the client when the API schema changes (stale types)
❌ Wrong — server adds a required field; TypeScript client still sends the old request shape; runtime errors.
✅ Correct — add client regeneration to the CI pipeline; treat the generated client as a build artefact.