NSwag — Generating TypeScript Clients for Angular

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);
//   }
// }
Note: The NSwag-generated client uses Angular’s 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.
Tip: Add the NSwag client generation as a CI step that runs after the build and commits any generated file changes. If the API schema changed but the generated client was not regenerated, the CI pipeline catches the inconsistency: the generated client file differs from what would be generated fresh, signalling that the client needs to be updated. Use a shell script: nswag run nswag.json && git diff --exit-code src/app/api/client.ts || (echo "Generated client is out of date" && exit 1).
Warning: Never manually edit NSwag-generated files. They will be overwritten on the next generation run, losing your edits. If the generated client does not handle a specific scenario (custom error handling, request interceptors), extend it in a separate Angular service that wraps the generated client: @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.

🧠 Test Yourself

The server renames the response field authorName to author.name (nested object) in the API. The Angular NSwag-generated client is regenerated. What happens to Angular code that reads post.authorName?