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 itsControllersuffix, the action name and the concrete version from[ApiVersion]. An unsubstituted token or a bare template fragment such as{classCode}/rateis 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 theMap<Verb>pattern composed with anyMapGroupprefix, and an endpoint that exists only as a delegate inProgram.csis easy to skip. - The route composition table matches the registration: the template on each
controller, which controllers use
{version:apiVersion}and which use a literalv1prefix, 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.mdor 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-001string is a contract violation. ## Endpoints by operationappears only when an endpoint really maps to a generated operation document, and is omitted otherwise.- No Framework4 vocabulary appears:
RoutePrefix,WebApiConfigandHttpConfigurationdo 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`