API Documentation Best Practices — Contracts and Changelogs

An API is only as useful as its documentation is accurate, current, and discoverable. Documentation that lags behind the implementation, has no examples, or omits error cases forces Angular developers to probe the API empirically — slowing integration and creating mismatches. Treating documentation as a first-class concern alongside code (reviewed in PRs, updated with every feature, versioned alongside the API) is what separates APIs that developers love from those they dread.

Documentation Standards

// ── Complete controller documentation template ─────────────────────────────

/// <summary>
/// Manages blog posts — CRUD operations, publishing, and search.
/// Authentication: JWT Bearer token required for write operations.
/// </summary>
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
[Consumes("application/json")]
public class PostsController : ControllerBase
{
    /// <summary>
    /// Returns a paginated list of published posts.
    /// </summary>
    /// <param name="page">Page number (1-based). Default: 1.</param>
    /// <param name="size">Items per page (1-100). Default: 10.</param>
    /// <param name="search">Optional full-text search query.</param>
    /// <param name="category">Optional category slug filter.</param>
    /// <returns>Paginated list of post summaries.</returns>
    /// <response code="200">Successfully returned the post list.</response>
    /// <response code="400">Invalid pagination parameters.</response>
    [HttpGet]
    [AllowAnonymous]
    [ProducesResponseType(typeof(PagedResult<PostSummaryDto>), 200)]
    [ProducesResponseType(typeof(ValidationProblemDetails),   400)]
    public async Task<ActionResult<PagedResult<PostSummaryDto>>> GetAll(
        [FromQuery, Range(1, int.MaxValue)]    int    page     = 1,
        [FromQuery, Range(1, 100)]             int    size     = 10,
        [FromQuery, StringLength(100)]         string search   = "",
        [FromQuery, RegularExpression(@"^[a-z0-9-]*$")] string category = "",
        CancellationToken ct = default)
        => Ok(await _service.GetPublishedAsync(page, size, search, category, ct));
}

// ── API Changelog format (CHANGELOG.md) ──────────────────────────────────
// ## [2.0.0] - 2025-06-01
// ### Breaking Changes
// - `GET /api/posts/{id}`: `authorName` (string) replaced by `author` (object)
// - `POST /api/posts`: `tags` now required (was optional)
//
// ## [1.1.0] - 2025-03-15
// ### Added
// - `GET /api/posts/{id}/analytics` — view count and engagement metrics
// - `viewCount` field added to PostSummaryDto
//
// ## [1.0.0] - 2025-01-01
// ### Initial Release
Note: A public API changelog serves two purposes: it documents the history of changes for existing clients who need to migrate, and it demonstrates the API team’s professionalism and commitment to stability. The changelog should be published in the developer portal alongside the API reference. Use semantic versioning (MAJOR.MINOR.PATCH): MAJOR for breaking changes, MINOR for backwards-compatible features, PATCH for bug fixes. Clients know that upgrading from 1.x to 2.x requires migration effort; upgrading within 1.x is safe.
Tip: Publish your OpenAPI spec to a developer portal like readme.io, Stoplight, or Redocly. These platforms host interactive API documentation with “Try It” interfaces, code samples in multiple languages (curl, Python, Node.js, TypeScript), and changelog tracking. The spec is uploaded manually or via CI/CD on every deployment. A polished developer portal reduces the time for external developers (or new team members) to make their first successful API call from hours to minutes.
Warning: The Sunset header advertises when a deprecated version will be removed. Once you publish a sunset date, commit to it — removing an API version before the announced sunset date violates the trust of clients who planned their migration timeline around your announcement. Announce deprecation at least 6 months before sunset for production APIs. Keep the sunset date realistic: a v1 with thousands of active clients cannot be sunset in 30 days regardless of how eager you are to remove it.

Generating the OpenAPI Spec File

// ── Generate spec file from CLI (for CI pipeline) ─────────────────────────
// dotnet tool install --global NSwag.ConsoleCore
// nswag aspnetcore2openapi /project:src/BlogApp.Api/BlogApp.Api.csproj /output:swagger.json

// ── Or use the swashbuckle CLI ─────────────────────────────────────────────
// dotnet tool install --global Swashbuckle.AspNetCore.Cli
// swagger tofile --output swagger.json src/BlogApp.Api/bin/Release/net8.0/BlogApp.Api.dll v1

// ── CI step (GitHub Actions) ───────────────────────────────────────────────
// - name: Generate OpenAPI spec
//   run: |
//     swagger tofile --output swagger.json ${{ env.API_DLL }} v1
//     # Upload spec as CI artifact
//     # Trigger NSwag client regeneration in Angular project
//
// ── Validate spec has not broken existing contracts ────────────────────────
// dotnet tool install --global openapi-diff
// openapi-diff --fail-on-incompatible old-swagger.json new-swagger.json
// # Returns exit code 1 if breaking changes detected — fails the PR build

Common Mistakes

Mistake 1 — Not updating docs when API changes (stale documentation loses developer trust)

❌ Wrong — new field added to response but ProducesResponseType still shows old schema; Swagger shows wrong type.

✅ Correct — treat documentation attributes as part of the change that must be reviewed in the PR.

Mistake 2 — No breaking change detection in CI (accidental breaking changes reach production)

❌ Wrong — field renamed in refactoring; no check detects the breaking change; Angular clients break after deployment.

✅ Correct — add openapi-diff or similar tool to CI that compares the new spec against the previous version and fails on breaking changes.

🧠 Test Yourself

The Sunset header on deprecated API v1 responses specifies Sat, 01 Jun 2026 00:00:00 GMT. It is now July 2026 and some clients are still using v1. What should you do?