Route design

Table of contents

The framework does not enforce a specific route layout, but these conventions work well with RESTyard’s link resolution and are used throughout the CarShack demo.

Entry point

Every API should have a single entry point that links to all top-level resources. Clients bookmark this one URL and discover everything else through links.

GET /entrypoint
[HypermediaObject(Title = "Entry to the API", Classes = new[] { "Entrypoint" })]
public class EntrypointHto : IHypermediaObject
{
    [Relations([DefaultHypermediaRelations.Self])]
    public ILink<EntrypointHto> Self { get; set; }

    [Relations(["Customers"])]
    public ILink<HypermediaCustomersRootHto> Customers { get; set; }

    [Relations(["Cars"])]
    public ILink<HypermediaCarsRootHto> Cars { get; set; }
}

The entry point is the only URL a client needs to know. All other URLs are discovered via links.

Collection roots

Collections like “Customers” are accessed through a root HTO, not by returning a list directly. The root HTO handles collection-level actions (create, query) and links to individual entities.

GET  /Customers                → CustomersRootHto (links, actions, metadata)
POST /Customers/CreateCustomer → creates a customer, returns Location header
POST /Customers/Queries        → creates a query, returns Location header to result
GET  /Customers/Query?...      → query result with embedded entities

This avoids returning a potentially large list of entities on the collection URL itself. The client first sees what actions are available, then explicitly requests data through a query action.

Entities and actions

Individual entities are sub-routes of their collection. Actions on an entity are sub-routes of the entity. This ensures route template variables from the entity route are available for its actions.

GET  /Customers/{key}       → single customer HTO
POST /Customers/{key}/Move  → action on that customer

Actions must be sub-routes of their owning HTO so the framework can fill route template variables. A Move action on customer 42 needs the {key} variable from /Customers/{key}.

Flat hierarchy

Do not nest entities under other entities. If a customer has orders, put orders in their own collection rather than under /Customers/{key}/Orders/{orderId}. This keeps controllers simple and routes shallow.

GET /Customers/{key}   → customer HTO (with a link to their orders)
GET /Orders/{key}      → order HTO

The relationship between customer and orders is expressed through links in the Siren document, not through URL nesting.

Queries and pagination

Clients should never build query strings manually. Instead, they post a query object to an action endpoint and follow the Location header to the result.

POST /Customers/Queries          → client posts query parameters as JSON
  ← 201 Location: /Customers/Query?filter=...&page=0&pageSize=10
GET  /Customers/Query?filter=... → paginated result

The query result HTO includes navigation links for pagination:

[Relations(["next"])]
public ILink<HypermediaCustomerQueryResultHto>? Next { get; set; }

[Relations(["previous"])]
public ILink<HypermediaCustomerQueryResultHto>? Previous { get; set; }

[Relations(["last"])]
public ILink<HypermediaCustomerQueryResultHto>? Last { get; set; }

[Relations(["all"])]
public ILink<HypermediaCustomerQueryResultHto>? All { get; set; }

Nullable links naturally disappear from the Siren output — Next is null on the last page, Previous is null on the first page.

Route template variables

Use {key} as the conventional name for single-key routes. For multi-key routes, use descriptive names matching the HTO’s [Key] attributes:

// Single key
[HttpGet("{key:int}"), HypermediaObjectEndpoint<HypermediaCustomerHto>]

// Multiple keys
[HttpGet("{brand}/{key:int}"), HypermediaObjectEndpoint<HypermediaCarHto>]

See Endpoints — Routes with a placeholder for how to map HTO properties to route variables.

Summary

Pattern Route Purpose
Entry point GET /entrypoint Single API entry, links to everything
Collection root GET /Customers Metadata, collection-level actions
Create action POST /Customers/CreateCustomer Returns Location to created entity
Query action POST /Customers/Queries Returns Location to query result
Query result GET /Customers/Query?... Paginated results with nav links
Entity GET /Customers/{key} Single entity with actions
Entity action POST /Customers/{key}/Move Action on a specific entity