software · dotnet · api

Separating Public and Internal API Documentation in .NET

  • dotnet
  • swagger
  • api
  • architecture

Separating Public and Internal API Documentation in .NET

In many services there are two types of endpoints:

  • Public APIs used by customers or external systems
  • Internal APIs used for operations, debugging, maintenance, or service-to-service communication

When all endpoints appear in a single Swagger document, the API surface becomes harder to understand and internal endpoints may accidentally be exposed to consumers.

A simple solution is to publish multiple Swagger documents from the same service.

API Documentation Structure

Instead of a single Swagger definition, we define two:


/swagger/public/swagger.json
/swagger/internal/swagger.json

  • Public → Customer-facing APIs
  • Internal → Operational or system endpoints

Swagger UI can display both documents separately.

Controller Grouping

Controllers can be assigned to a documentation group using ApiExplorerSettings.

Example:

[ApiController]
[Route("api/internal/[controller]")]
[ApiExplorerSettings(GroupName = "internal")]
public class AdminController : ControllerBase
{
    [HttpGet("health-detail")]
    public IActionResult Get()
    {
        return Ok("internal data");
    }
}

Public endpoints can use a different group:

[ApiController]
[Route("api/[controller]")]
[ApiExplorerSettings(GroupName = "public")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok();
    }
}

Swagger Configuration

Multiple documents are registered in SwaggerGen.

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("public", new OpenApiInfo
    {
        Title = "Public API",
        Version = "v1"
    });

    options.SwaggerDoc("internal", new OpenApiInfo
    {
        Title = "Internal API",
        Version = "v1"
    });

    options.DocInclusionPredicate((docName, apiDesc) =>
    {
        var groupName = apiDesc.GroupName;
        return string.Equals(groupName, docName, StringComparison.OrdinalIgnoreCase);
    });
});

And both endpoints are exposed in Swagger UI:

app.UseSwagger();

app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/swagger/public/swagger.json", "Public API");
    options.SwaggerEndpoint("/swagger/internal/swagger.json", "Internal API");
});

Important Note

This approach only separates the documentation, not the access.

Internal endpoints should still be protected using:

  • Authentication / Authorization policies
  • API gateway rules
  • Network restrictions
  • Internal service routing

Why This Helps

Separating internal and external APIs improves:

  • API clarity for external consumers
  • Operational visibility for internal teams
  • Safer documentation exposure

In larger platforms, this pattern helps keep operational endpoints from being mixed with the public API surface.