NSwag - A Game Changer for ASP.NET Core and Angular Developers
By Alessandro Rosà, Tue Feb 24 2026
Contents
- Introduction
- What is NSwag?
- Why NSwag is Excellent for Web API Developers
- Why NSwag is Excellent for Angular Developers
- Setting Up NSwag
- Real-World Usage
- Advanced Features
- Benefits in Practice
- Integration with CI/CD
- Alternatives and Comparison
- Common Pitfalls and Solutions
- Conclusion
- Resources
Introduction
If you've ever worked on a project with an ASP.NET Core Web API backend and an Angular frontend, you know the pain of keeping your API contracts in sync. Manually writing HTTP services, managing DTOs on both ends, and updating them every time the API changes is tedious and error-prone.
Enter NSwag - a toolchain that generates TypeScript clients (and much more) directly from your ASP.NET Core Web API. After using NSwag extensively in production, I can confidently say it's one of the best productivity boosters for full-stack .NET development.
What is NSwag?
NSwag is a Swagger/OpenAPI toolchain for .NET that:
- Generates OpenAPI specifications from your ASP.NET Core Web APIs
- Creates TypeScript/JavaScript clients for Angular, React, and other frameworks
- Generates C# clients for consuming APIs
- Provides a UI for testing and documenting your APIs
Think of it as the bridge that connects your backend and frontend, ensuring they always speak the same language.
Why NSwag is Excellent for Web API Developers
1. Automatic API Documentation
With NSwag, your API documentation writes itself. Simply add XML comments to your controllers and models:
/// <summary>
/// Retrieves a product by its ID
/// </summary>
/// <param name="id">The product identifier</param>
/// <returns>The product details</returns>
/// <response code="200">Returns the product</response>
/// <response code="404">Product not found</response>
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
var product = await _mediator.Send(new GetProductByIdQuery { Id = id });
return product != null ? Ok(product) : NotFound();
}
NSwag generates beautiful, interactive documentation from these comments automatically.
2. Design-First or Code-First - Your Choice
NSwag supports both approaches:
Code-First: Write your API, and NSwag generates the OpenAPI spec Design-First: Create an OpenAPI spec first, and generate server stubs
For most ASP.NET Core projects, code-first is the natural choice, and NSwag excels at it.
3. Type Safety at the API Boundary
By decorating your endpoints properly, NSwag understands exactly what your API expects and returns:
public class ProductDto
{
/// <summary>
/// The unique product identifier
/// </summary>
public int Id { get; set; }
/// <summary>
/// Product name
/// </summary>
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Product price in USD
/// </summary>
[Range(0.01, 999999.99)]
public decimal Price { get; set; }
/// <summary>
/// Available stock quantity
/// </summary>
public int Stock { get; set; }
}
These attributes become part of the OpenAPI specification and are enforced on the client side.
4. Versioning Support
NSwag handles API versioning elegantly:
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<List<ProductDtoV1>>> GetProductsV1()
{
// Version 1 implementation
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<ActionResult<List<ProductDtoV2>>> GetProductsV2()
{
// Version 2 implementation with new fields
}
}
NSwag generates separate clients for each API version, making migrations smooth.
Why NSwag is Excellent for Angular Developers
1. Type-Safe HTTP Calls
Forget manually typing HTTP responses. NSwag generates fully typed Angular services:
// Generated by NSwag
export class ProductsClient {
constructor(private http: HttpClient, @Inject(API_BASE_URL) private baseUrl?: string) {}
/**
* Retrieves a product by its ID
* @param id The product identifier
* @return Returns the product
*/
getProduct(id: number): Observable<ProductDto> {
let url_ = this.baseUrl + "/api/products/{id}";
url_ = url_.replace("{id}", encodeURIComponent("" + id));
return this.http.get<ProductDto>(url_).pipe(
map((response: any) => {
return ProductDto.fromJS(response);
})
);
}
}
export class ProductDto implements IProductDto {
id!: number;
name!: string;
price!: number;
stock!: number;
constructor(data?: IProductDto) {
if (data) {
Object.assign(this, data);
}
}
static fromJS(data: any): ProductDto {
data = typeof data === 'object' ? data : {};
let result = new ProductDto();
result.init(data);
return result;
}
}
2. IntelliSense and Auto-Completion
Your IDE knows exactly what the API returns:
// Full IntelliSense support!
this.productsClient.getProduct(123).subscribe(product => {
console.log(product.name); // ✓ TypeScript knows this exists
console.log(product.price); // ✓ Knows it's a number
console.log(product.invalid); // ✗ Compilation error!
});
No more typos, no more runtime errors from accessing non-existent properties.
3. Automatic DTO Synchronization
When backend DTOs change, regenerate the client, and TypeScript immediately shows you what broke:
Backend change:
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
public string Category { get; set; } = string.Empty; // NEW FIELD
}
After regeneration, TypeScript tells you:
// Compilation error: Property 'category' is missing
const productDisplay: ProductDto = {
id: 1,
name: "Product",
price: 99.99,
stock: 10
// Missing: category
};
4. Handles Complex Scenarios
NSwag properly generates code for:
- Enums: Become TypeScript enums
- Nullable types: Optional properties in TypeScript
- File uploads/downloads: Proper handling of FormData and Blobs
- Generic types: Correctly typed
- Inheritance: Maintains class hierarchies
Setting Up NSwag
Backend Setup (ASP.NET Core)
1. Install NuGet packages:
dotnet add package NSwag.AspNetCore
2. Configure in Program.cs:
builder.Services.AddOpenApiDocument(config =>
{
config.Title = "My API";
config.Version = "v1";
config.Description = "API for managing products";
});
// After app.Build():
app.UseOpenApi(); // Serves the OpenAPI/Swagger spec
app.UseSwaggerUi(); // Serves the Swagger UI
3. Enable XML documentation:
Edit your .csproj:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
Frontend Setup (Angular)
1. Install NSwag CLI:
npm install nswag --save-dev
2. Create nswag.json configuration:
{
"runtime": "Net80",
"defaultVariables": null,
"documentGenerator": {
"fromDocument": {
"url": "http://localhost:5000/swagger/v1/swagger.json",
"output": null
}
},
"codeGenerators": {
"openApiToTypeScriptClient": {
"className": "{controller}Client",
"moduleName": "",
"template": "Angular",
"promiseType": "Promise",
"httpClass": "HttpClient",
"injectionTokenType": "InjectionToken",
"rxJsVersion": 7.0,
"dateTimeType": "Date",
"generateClientClasses": true,
"generateClientInterfaces": false,
"generateOptionalParameters": true,
"wrapDtoExceptions": true,
"useTransformOptionsMethod": false,
"useTransformResultMethod": false,
"generateDtoTypes": true,
"operationGenerationMode": "SingleClientFromOperationId",
"markOptionalProperties": true,
"typeScriptVersion": 5.0,
"output": "src/app/api/api-client.ts"
}
}
}
3. Add script to package.json:
{
"scripts": {
"generate-client": "nswag run nswag.json"
}
}
4. Generate the client:
npm run generate-client
Real-World Usage
Backend Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// Get all products with pagination
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResult<ProductDto>>> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
var query = new GetAllProductsQuery { Page = page, PageSize = pageSize };
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// Create a new product
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ProductDto>> CreateProduct(
[FromBody] CreateProductCommand command)
{
var result = await _mediator.Send(command);
return CreatedAtAction(nameof(GetProduct), new { id = result.Id }, result);
}
}
Frontend Angular Service
import { Injectable } from '@angular/core';
import { ProductsClient, ProductDto, CreateProductCommand } from './api/api-client';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor(private productsClient: ProductsClient) {}
getProducts(page: number = 1, pageSize: number = 10): Observable<PagedResult<ProductDto>> {
return this.productsClient.getProducts(page, pageSize);
}
createProduct(name: string, price: number, stock: number): Observable<ProductDto> {
const command = new CreateProductCommand({
name,
price,
stock
});
return this.productsClient.createProduct(command);
}
}
Angular Component
@Component({
selector: 'app-products',
template: `
<div *ngFor="let product of products">
<h3>{{ product.name }}</h3>
<p>Price: {{ product.price | currency }}</p>
<p>Stock: {{ product.stock }}</p>
</div>
`
})
export class ProductsComponent implements OnInit {
products: ProductDto[] = [];
constructor(private productService: ProductService) {}
ngOnInit() {
this.productService.getProducts().subscribe(result => {
this.products = result.items; // Fully typed!
});
}
}
Advanced Features
Custom Template Customization
You can customize the generated TypeScript code using liquid templates:
{
"codeGenerators": {
"openApiToTypeScriptClient": {
"templateDirectory": "./nswag-templates",
"typeScriptVersion": 5.0
}
}
}
Authentication Handling
NSwag properly handles authentication:
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// Endpoints require authentication
}
Generated client includes authorization headers:
// In your Angular module
@NgModule({
providers: [
{
provide: API_BASE_URL,
useValue: 'http://localhost:5000'
},
ProductsClient
]
})
export class AppModule {
constructor(private http: HttpClient) {
// Add interceptor for auth tokens
}
}
Error Handling
NSwag generates proper error handling:
this.productsClient.getProduct(123).subscribe({
next: (product) => {
console.log('Success:', product);
},
error: (error) => {
if (error.status === 404) {
console.log('Product not found');
} else if (error.status === 401) {
console.log('Unauthorized');
}
}
});
Benefits in Practice
For Backend Developers
- Less documentation writing - Code comments become documentation
- Contract-first development - API contract is always up-to-date
- Fewer support requests - Frontend devs have accurate documentation
- Easier testing - Swagger UI for manual testing
For Frontend Developers
- No manual HTTP code - Generated clients handle everything
- Type safety - Catch errors at compile-time, not runtime
- Better refactoring - TypeScript tells you what broke
- Faster development - Less boilerplate to write
- IntelliSense everywhere - IDE knows your entire API
For Teams
- Single source of truth - The backend defines the contract
- Faster iterations - Change API, regenerate client, done
- Fewer bugs - Type mismatches caught early
- Better collaboration - Backend and frontend stay in sync
- Onboarding made easy - New devs see the entire API instantly
Integration with CI/CD
Automate client generation in your pipeline:
# Azure DevOps example
- task: Npm@1
displayName: 'Generate API Client'
inputs:
command: 'custom'
customCommand: 'run generate-client'
workingDir: './ClientApp'
Alternatives and Comparison
NSwag vs Swagger Codegen
- NSwag: Better .NET integration, more customizable templates
- Swagger Codegen: More language support, but less .NET-focused
NSwag vs OpenAPI Generator
- NSwag: Better for .NET + TypeScript, excellent Angular support
- OpenAPI Generator: Community-driven, broader language support
NSwag vs Manual HTTP Services
- NSwag: Type-safe, automated, less error-prone
- Manual: Full control, but tedious and error-prone
For .NET + Angular stacks, NSwag is the clear winner.
Common Pitfalls and Solutions
Issue: Generated Code is Outdated
Solution: Add a pre-build step to regenerate clients:
"scripts": {
"prebuild": "npm run generate-client"
}
Issue: Large Generated Files
Solution: Split APIs into multiple controllers and generate separate clients:
{
"codeGenerators": {
"openApiToTypeScriptClient": {
"operationGenerationMode": "MultipleClientsFromPathSegments"
}
}
}
Issue: Complex Types Not Generating Correctly
Solution: Use [JsonConverter] or custom type mappings in NSwag config.
Conclusion
NSwag has fundamentally changed how I develop full-stack .NET applications. The time saved from not writing and maintaining HTTP services manually is enormous. More importantly, the confidence that comes from compile-time type safety across the entire stack is invaluable.
For ASP.NET Core Web API developers, NSwag provides excellent documentation and client generation with minimal effort. For Angular developers, it eliminates an entire category of runtime errors and makes consuming APIs a breeze.
If you're building a .NET backend with an Angular frontend and not using NSwag, you're missing out on one of the best productivity tools in the ecosystem.
Resources
- NSwag GitHub Repository
- NSwag Documentation
- ASP.NET Core OpenAPI Documentation
- NSwag Studio - GUI tool for configuration
Happy coding! 🚀