NSwag - A Game Changer for ASP.NET Core and Angular Developers

By Alessandro Rosà, Tue Feb 24 2026

Contents

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

  1. Less documentation writing - Code comments become documentation
  2. Contract-first development - API contract is always up-to-date
  3. Fewer support requests - Frontend devs have accurate documentation
  4. Easier testing - Swagger UI for manual testing

For Frontend Developers

  1. No manual HTTP code - Generated clients handle everything
  2. Type safety - Catch errors at compile-time, not runtime
  3. Better refactoring - TypeScript tells you what broke
  4. Faster development - Less boilerplate to write
  5. IntelliSense everywhere - IDE knows your entire API

For Teams

  1. Single source of truth - The backend defines the contract
  2. Faster iterations - Change API, regenerate client, done
  3. Fewer bugs - Type mismatches caught early
  4. Better collaboration - Backend and frontend stay in sync
  5. 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

Happy coding! 🚀

Back home