Skip to content

API Endpoint Naming Conventions

All Microtec ERP REST APIs follow a consistent naming convention derived from the Clean Architecture CQRS handler names. Consistent naming makes API exploration predictable and simplifies gateway routing configuration.


Base URL Structure

https://gateway.{domain}/api/v{version}/{module}/{resource}
PartDescriptionExample
gateway.{domain}API Gateway hostnamegateway.microtecstage.com
/apiFixed prefix (discriminates API from auth routes)/api
/v{version}Version prefix/v1
/{module}ERP module (optional, used in multi-module services)/accounting
/{resource}Plural noun identifying the resource/invoices

Standard Operation Patterns

Every resource exposes a consistent set of operations. Backend handler naming maps directly to HTTP verbs and URL patterns:

CQRS HandlerHTTP MethodURL PatternDescription
AddInvoiceCommandPOST/api/v1/invoicesCreate a new invoice
EditInvoiceCommandPUT/api/v1/invoices/{id}Update an existing invoice
GetByIdInvoiceQueryGET/api/v1/invoices/{id}Get a single invoice by ID
GetAllInvoiceQueryGET/api/v1/invoicesGet paginated list
GetDropdownInvoiceQueryGET/api/v1/invoices/dropdownLightweight list for select inputs
ExportInvoiceQueryGET/api/v1/invoices/exportExport to Excel/PDF

Handler Naming → Route Naming

The CQRS handler name is the canonical source of truth. When you see AddInvoiceCommand, you know the endpoint is POST /api/v1/invoices. When you see GetDropdownCostCenterQuery, you know the endpoint is GET /api/v1/cost-centers/dropdown. This predictability allows code generation and gateway config to be automated.


Route Naming Rules

Resources Are Plural Nouns

/api/v1/invoices          ✓
/api/v1/invoice           ✗
/api/v1/getInvoice        ✗
/api/v1/invoice/list      ✗

Multi-Word Resources Use Kebab-Case

/api/v1/cost-centers      ✓
/api/v1/costCenters       ✗
/api/v1/cost_centers      ✗
/api/v1/CostCenters       ✗

IDs Are Path Parameters

GET /api/v1/invoices/42   ✓  (get invoice 42)
GET /api/v1/invoices?id=42  ✗  (IDs belong in the path)

Filter and Pagination Are Query Parameters

GET /api/v1/invoices?page=1&pageSize=20&status=Draft&fromDate=2024-01-01

Standard query parameters supported by all GetAll endpoints:

ParameterTypeDefaultDescription
pageint1Page number (1-based)
pageSizeint20Items per page (max 100)
searchstringFull-text search
sortBystringidField to sort by
sortDirstringascSort direction: asc or desc
fromDatedateFilter by start date
toDatedateFilter by end date

Special Endpoint Patterns

Returns a minimal projection optimised for use in Angular <select> and PrimeNG dropdown components:

GET /api/v1/accounts/dropdown

Response:

json
[
  { "id": 1, "name": "Cash", "code": "1001" },
  { "id": 2, "name": "Accounts Receivable", "code": "1200" }
]

Export

Returns a file download (Excel or PDF):

GET /api/v1/invoices/export?format=excel&fromDate=2024-01-01&toDate=2024-01-31

Response: 200 OK with Content-Disposition: attachment; filename=invoices.xlsx and Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.

Bulk Operations

POST /api/v1/invoices/bulk-approve   (approve multiple by ID array)
POST /api/v1/invoices/bulk-delete    (delete multiple by ID array)

Request body:

json
{ "ids": [1, 2, 3, 4] }

State Transitions

Use a verb sub-resource for state-changing actions (not PUT, which implies full replacement):

POST /api/v1/invoices/{id}/approve
POST /api/v1/invoices/{id}/void
POST /api/v1/invoices/{id}/submit

Response Envelope

All endpoints return a consistent JSON envelope:

json
{
  "success": true,
  "data": { ... },
  "message": null,
  "errors": null
}

For paginated list responses:

json
{
  "success": true,
  "data": {
    "items": [ ... ],
    "total": 247,
    "page": 1,
    "pageSize": 20,
    "totalPages": 13
  }
}

For error responses:

json
{
  "success": false,
  "data": null,
  "message": "Invoice not found",
  "errors": {
    "id": ["The invoice with ID 99 does not exist"]
  }
}

Controller Template

csharp
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
[Produces("application/json")]
public class InvoicesController : ControllerBase
{
    private readonly IMediator _mediator;

    public InvoicesController(IMediator mediator) => _mediator = mediator;

    [HttpGet]
    [ProducesResponseType(typeof(ApiResponse<PagedResult<InvoiceDto>>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll([FromQuery] GetAllInvoiceQuery query)
        => Ok(await _mediator.Send(query));

    [HttpGet("{id:int}")]
    [ProducesResponseType(typeof(ApiResponse<InvoiceDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(int id)
        => Ok(await _mediator.Send(new GetByIdInvoiceQuery(id)));

    [HttpGet("dropdown")]
    [ProducesResponseType(typeof(ApiResponse<List<DropdownDto>>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetDropdown()
        => Ok(await _mediator.Send(new GetDropdownInvoiceQuery()));

    [HttpPost]
    [ProducesResponseType(typeof(ApiResponse<int>), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Add([FromBody] AddInvoiceCommand command)
    {
        var id = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetById), new { id }, ApiResponse.Ok(id));
    }

    [HttpPut("{id:int}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Edit(int id, [FromBody] EditInvoiceCommand command)
    {
        command = command with { Id = id };
        await _mediator.Send(command);
        return NoContent();
    }

    [HttpGet("export")]
    [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
    public async Task<IActionResult> Export([FromQuery] ExportInvoiceQuery query)
    {
        var file = await _mediator.Send(query);
        return File(file.Content, file.ContentType, file.FileName);
    }
}

Versioning

The current API version is v1. When breaking changes are required:

  1. Create a new v2 controller alongside the existing v1 controller
  2. Both versions run simultaneously during the transition period
  3. Announce deprecation of v1 to API consumers with a minimum 90-day notice
  4. Remove v1 after the deprecation period

See Swagger documentation for how multiple API versions are exposed in the Swagger UI.

Internal Documentation — Microtec Platform Team