Skip to content

Example: ASP.NET Core API endpoint catalog

endpoints.md is the hub for an ASP.NET Core API's HTTP surface. It explains how a route is composed in this particular service, lists every controller and minimal API group, and carries one catalog row per endpoint that links into the per-controller detail pages. Doc gen produces one per API at docs/apis/<api-slug>/endpoints.md.

What reviewers should check

  • Every route in the catalog is fully resolved: [controller], [action] and {version:apiVersion} are replaced with the controller name minus its Controller suffix, the action name and the concrete version from [ApiVersion]. An unsubstituted token or a bare template fragment such as {classCode}/rate is a contract violation.
  • A controller that declares several API versions has one catalog row per version, and the Endpoints column of the controller catalog counts routes, not actions.
  • Minimal API endpoints (Map<Verb>) are in the catalog. Their route is the Map<Verb> pattern composed with any MapGroup prefix, and an endpoint that exists only as a delegate in Program.cs is easy to skip.
  • The route composition table matches the registration: the template on each controller, which controllers use {version:apiVersion} and which use a literal v1 prefix, and whether any convention route survives.
  • Each Endpoint cell links to that endpoint's H2 anchor on the controller's page (endpoints/<controller-slug>.md#<verb>-<route-slug>), never to an on-page anchor and never to the top of the controller page.
  • The Auth column links into authentication-authorization.md or records "None in code". An unprotected endpoint is a finding, not an omission to be tidied away. The Errors column lists the statuses the code declares; a response produced by an opaque package pipeline is not invented here.
  • The Rules column cites business rules as links. A bare BR-001 string is a contract violation.
  • ## Endpoints by operation appears only when an endpoint really maps to a generated operation document, and is omitted otherwise.
  • No Framework4 vocabulary appears: RoutePrefix, WebApiConfig and HttpConfiguration do not exist in an ASP.NET Core API.

Example

The document below is a complete, correct api-endpoints.md for a fictional ASP.NET Core API.

View rendered example — the same document as a real page, so you can compare rendering as well as source.

---
title: Endpoints
summary: Endpoint catalog for the REST API, with per-controller detail pages.
status: draft
owner: Premium Systems Team
last_reviewed: 2026-10-21
---

# Endpoints

`SAIF.Premium.Api` exposes eight routes across three controllers and one minimal
API group: premium reads and adjustments, class-code rates, an administrative
batch recalculation, and the current rate table. Seven of the eight routes are
distinct actions; `ClassCodesController` serves a single action at both `v1` and
`v2`, so it appears twice. Controller routes are attribute routes that combine a
class-level `[Route]` template with the action's `[Http<Verb>]` template, and
the version segment is either substituted from `[ApiVersion]` or written
literally as `v1`.

This page is the endpoint catalog and hub. Each controller's endpoints are documented in detail on its own page under `endpoints/`.

## Route composition

| Aspect | Finding |
| --- | --- |
| Routing mode | Attribute routes (`[ApiController]` + `[Route]`) for the three controllers; Minimal API (`MapGet`) for `RateTableEndpoints` |
| Controller route template | `[Route("v{version:apiVersion}/[controller]")]` on `PremiumsController` and `ClassCodesController`; `[Route("v1/[controller]/[action]")]` on `AdminController` |
| Versioning | URL segment `{version:apiVersion}` from `[ApiVersion("1.0")]` on `PremiumsController` and from `[ApiVersion("1.0")]` plus `[ApiVersion("2.0")]` on `ClassCodesController`; literal `v1` prefix on `AdminController` and `RateTableEndpoints` - see [Hosting pipeline](hosting-pipeline.md#api-versioning) |
| Base path | `/` (IIS application path not determined from source) |

A resolved route is the controller's `[Route]` template with its tokens
substituted, followed by the action's template. `[controller]` is replaced by the
controller name without the `Controller` suffix, keeping its casing, and
`{version:apiVersion}` by the concrete version. `PremiumsController` carries
`[Route("v{version:apiVersion}/[controller]")]` and `[ApiVersion("1.0")]`, so
`[HttpGet("{policyNumber}/history")]` on `GetPremiumHistory` resolves to
`GET /v1/Premiums/{policyNumber}/history`. `ClassCodesController` declares
versions `1.0` and `2.0`, so its one action resolves to both
`GET /v1/ClassCodes/{classCode}/rate` and `GET /v2/ClassCodes/{classCode}/rate`.

`AdminController` writes the version literally and uses the `[action]` token:
`[Route("v1/[controller]/[action]")]` with a bare `[HttpPost]` on
`RecalculateBatch` resolves to `POST /v1/Admin/RecalculateBatch`. The minimal
API group composes its `MapGroup` prefix with the `MapGet` pattern:
`MapGroup("/v1/RateTables")` plus `MapGet("/current", ...)` resolves to
`GET /v1/RateTables/current`.

`GET /v1/Premiums/estimate` and `GET /v1/Premiums/{policyNumber}` have the same
number of segments. ASP.NET Core ranks the literal segment above the parameter,
so `estimate` is never bound as a policy number.

## Controller catalog

| Controller | Route template | API version | Base | Endpoints | Documentation |
| --- | --- | --- | --- | --- | --- |
| `PremiumsController` | `v{version:apiVersion}/[controller]` | `1.0` | `ControllerBase` | 4 | [Details](endpoints/premiums.md) |
| `ClassCodesController` | `v{version:apiVersion}/[controller]` | `1.0`, `2.0` | `ControllerBase` | 2 | [Details](endpoints/class-codes.md) |
| `AdminController` | `v1/[controller]/[action]` | - | `ControllerBase` | 1 | [Details](endpoints/admin.md) |
| `RateTableEndpoints` (minimal API group) | `MapGroup("/v1/RateTables")` | - | - | 1 | [Details](endpoints/rate-tables.md) |

## Endpoint catalog

| Endpoint | Controller | Action | Auth | Success | Errors | Rules |
| --- | --- | --- | --- | --- | --- | --- |
| [`GET /v1/Premiums/{policyNumber}`](endpoints/premiums.md#get-v1premiumspolicynumber) | `PremiumsController` | `GetPremium` | [PremiumReadResource](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-001](business-rules.md#br-001-premium-is-never-below-the-policy-minimum), [BR-004](business-rules.md#br-004-the-rate-table-in-force-on-the-effective-date-is-used) |
| [`GET /v1/Premiums/{policyNumber}/history`](endpoints/premiums.md#get-v1premiumspolicynumberhistory) | `PremiumsController` | `GetPremiumHistory` | [PremiumReadResource](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-003](business-rules.md#br-003-premium-history-excludes-voided-terms) |
| [`GET /v1/Premiums/estimate`](endpoints/premiums.md#get-v1premiumsestimate) | `PremiumsController` | `EstimatePremium` | [PremiumReadResource](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `500` | [BR-001](business-rules.md#br-001-premium-is-never-below-the-policy-minimum), [BR-004](business-rules.md#br-004-the-rate-table-in-force-on-the-effective-date-is-used) |
| [`POST /v1/Premiums/{policyNumber}/adjustments`](endpoints/premiums.md#post-v1premiumspolicynumberadjustments) | `PremiumsController` | `AddAdjustment` | [PremiumAdjustResource](authentication-authorization.md#roles-and-resources) | `201 Created` | `400`, `404`, `409`, `500` | [BR-002](business-rules.md#br-002-an-adjustment-may-not-exceed-25-percent-of-the-base-premium) |
| [`GET /v1/ClassCodes/{classCode}/rate`](endpoints/class-codes.md#get-v1classcodesclasscoderate) | `ClassCodesController` | `GetClassCodeRate` | [PremiumReadResource](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-004](business-rules.md#br-004-the-rate-table-in-force-on-the-effective-date-is-used) |
| [`GET /v2/ClassCodes/{classCode}/rate`](endpoints/class-codes.md#get-v2classcodesclasscoderate) | `ClassCodesController` | `GetClassCodeRate` | [PremiumReadResource](authentication-authorization.md#roles-and-resources) | `200 OK` | `400`, `404`, `500` | [BR-004](business-rules.md#br-004-the-rate-table-in-force-on-the-effective-date-is-used) |
| [`POST /v1/Admin/RecalculateBatch`](endpoints/admin.md#post-v1adminrecalculatebatch) | `AdminController` | `RecalculateBatch` | [PremiumAdminResource](authentication-authorization.md#roles-and-resources) | `202 Accepted` | `400`, `409`, `500` | [BR-005](business-rules.md#br-005-only-one-recalculation-batch-may-run-at-a-time) |
| [`GET /v1/RateTables/current`](endpoints/rate-tables.md#get-v1ratetablescurrent) | `RateTableEndpoints` | `GetCurrentRateTable` | None in code | `200 OK` | `500` | - |

The Errors column lists the statuses declared by `[ProducesResponseType]` and
thrown by the service. A caller that fails the `[SecurityApiAuthorize]` check
receives a response produced by the opaque security package; its status is not
visible in source and is not listed.

`RateTableEndpoints` carries no authorization attribute and no
`RequireAuthorization()` call, so `GET /v1/RateTables/current` is reachable by
any caller that can reach the service. See
[Authentication and authorization](authentication-authorization.md) for the
consequence.

## Request flow diagram

The following diagram visualizes how a request reaches a handling action.

```mermaid
flowchart TD
    REQ[Client request] --> PIPE["Middleware pipeline<br/>see hosting-pipeline.md"]
    PIPE --> ROUTE{Route match}
    ROUTE -->|GET /v1/Premiums/policyNumber| A["PremiumsController.GetPremium"]
    ROUTE -->|POST /v1/Premiums/policyNumber/adjustments| B["PremiumsController.AddAdjustment"]
    ROUTE -->|GET /v2/ClassCodes/classCode/rate| C["ClassCodesController.GetClassCodeRate"]
    ROUTE -->|POST /v1/Admin/RecalculateBatch| D["AdminController.RecalculateBatch"]
    ROUTE -->|GET /v1/RateTables/current| E["RateTableEndpoints.GetCurrentRateTable"]
    ROUTE -->|No match| NF[404 Not Found]
    A --> AUTH{SecurityApiAuthorize check}
    B --> AUTH
    C --> AUTH
    D --> AUTH
    AUTH -->|No| DENY[Denied by security package]
    AUTH -->|Yes| BIND{Model valid?}
    BIND -->|No| BAD[400 ValidationProblemDetails]
    BIND -->|Yes| OP[Premium service]
    OP --> OK[200, 201, or 202]
    OP --> ERR[404 or 409 from service exceptions]
    E --> OK
```

`RateTableEndpoints.GetCurrentRateTable` skips the authorization check because
the group declares none. The diagram shows one representative route per action;
the remaining `PremiumsController` routes follow the same path as `GetPremium`.

## Endpoints by operation

Endpoints that delegate to a documented processing workflow link to it here.

| Endpoint | Operation |
| --- | --- |
| [`POST /v1/Admin/RecalculateBatch`](endpoints/admin.md#post-v1adminrecalculatebatch) | [OP-001](operations/recalculate-premium-batch.md) |

## Related documentation
- [API index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Hosting pipeline](hosting-pipeline.md)
- [Authentication and authorization](authentication-authorization.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Data model](data-model.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

## Sources
- `src/SAIF.Premium.Api/Controllers/PremiumsController.cs`
- `src/SAIF.Premium.Api/Controllers/ClassCodesController.cs`
- `src/SAIF.Premium.Api/Controllers/AdminController.cs`
- `src/SAIF.Premium.Api/Endpoints/RateTableEndpoints.cs`
- `src/SAIF.Premium.Api/Program.cs`
- `src/SAIF.Premium.Api/Startup.cs`