Appearance
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}| Part | Description | Example |
|---|---|---|
gateway.{domain} | API Gateway hostname | gateway.microtecstage.com |
/api | Fixed 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 Handler | HTTP Method | URL Pattern | Description |
|---|---|---|---|
AddInvoiceCommand | POST | /api/v1/invoices | Create a new invoice |
EditInvoiceCommand | PUT | /api/v1/invoices/{id} | Update an existing invoice |
GetByIdInvoiceQuery | GET | /api/v1/invoices/{id} | Get a single invoice by ID |
GetAllInvoiceQuery | GET | /api/v1/invoices | Get paginated list |
GetDropdownInvoiceQuery | GET | /api/v1/invoices/dropdown | Lightweight list for select inputs |
ExportInvoiceQuery | GET | /api/v1/invoices/export | Export 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-01Standard query parameters supported by all GetAll endpoints:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (1-based) |
pageSize | int | 20 | Items per page (max 100) |
search | string | — | Full-text search |
sortBy | string | id | Field to sort by |
sortDir | string | asc | Sort direction: asc or desc |
fromDate | date | — | Filter by start date |
toDate | date | — | Filter by end date |
Special Endpoint Patterns
Dropdown
Returns a minimal projection optimised for use in Angular <select> and PrimeNG dropdown components:
GET /api/v1/accounts/dropdownResponse:
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-31Response: 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}/submitResponse 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:
- Create a new
v2controller alongside the existingv1controller - Both versions run simultaneously during the transition period
- Announce deprecation of
v1to API consumers with a minimum 90-day notice - Remove
v1after the deprecation period
See Swagger documentation for how multiple API versions are exposed in the Swagger UI.