CRMSeptember 2026 · 4 min read

Blazor CRM REST API Design

By go2ismail · Published · .NET 10

At a glance

A lead endpoint group under /api exposes list, detail, create, update, delete, and lookup actions. Every response is an ApiResponse envelope, and every route requires an authenticated JWT bearer token.

Trace the lead endpoint contract

The lead API in Indotalent’s Blazor CRM Source Code is a Minimal API group defined in Features/Pipeline/Lead/LeadEndpoint.cs. The Blazor client calls it through LeadService using RestSharp. The observed contract is compact and consistent:

EndpointMethodPurposeResponse value
/api/leadGETGrid rows for the data tableList of GetLeadListResponse
/api/lead/{id}GETSingle lead including audit fieldsGetLeadByIdResponse
/api/leadPOSTCreate a leadCreateLeadResponse (Id, code)
/api/lead/updatePOSTUpdate a leadUpdateLeadResponse
/api/lead/delete/{id}POSTDelete (soft delete) a leadSuccess flag
/api/lead/lookupGETLookup options for formsLeadLookupResponse

Notice the convention: create, update, and delete all use POST, and delete carries the id in the route rather than in the body. The GetLeadListResponse rows include the campaign title and sales team name through navigation includes, so the grid does not need a second round trip to display them.

See how routes are mounted

Program.cs creates the root once and attaches an endpoint filter that resolves the current user:

var apiGroup = app.MapGroup("/api").AddEndpointFilter<CurrentUserFilter>();
apiGroup.MapAccountEndpoints();
apiGroup.MapFeaturesEndpoint();

MapFeaturesEndpoint reaches each feature, and MapLeadEndpoints builds the nested group under it. Each endpoint resolves the IMediator, sends a command or query, and wraps the result with ToApiResponse. The handler file for each action carries its own request, response, and command record, which keeps the contract local to the operation.

Recognize the response envelope

All successful responses use the shared ApiResponse<T> model from Shared/Models/ApiResponse.cs. Its properties are IsSuccess, StatusCode, Message, Value, Pagination, Errors, and ServerTime. The endpoint extension builds the envelope, so a handler never returns a raw DTO: it returns the DTO or a null result, and the extension decides between a 200 OK with the value and a 404 Not Found.

The lookup endpoint is a good example of a composite response: LeadLookupResponse exposes Campaigns, SalesTeams, PipelineStages, and ClosingStatuses as lookup lists, so one request supplies every dropdown on the lead form.

Cross the authentication boundary

The lead group requires an authenticated user with the JWT bearer scheme:

var group = app.MapGroup("/lead").WithTags("Leads")
    .RequireAuthorization(policy => policy
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser());

The same pattern appears on campaign, budget, expense, lead-contact, lead-activity, sales-team, and sales-representative groups. The shared client base service attaches the bearer token and the current-user headers to every RestSharp request and attempts a token refresh before redirecting to login when a call returns 401.

Understand the validation boundaries

Field rules are defined with FluentValidation in the Cqrs folder, for example CreateLeadValidator requires Title, CompanyName, CampaignId, and SalesTeamId and validates the email format when present. The Blazor form attaches these validators directly to MudBlazor fields, so the UI validates on submit and per field.

The server pipeline is a separate boundary. MediatR is configured with a ValidationBehaviour, but that behavior looks for validators registered for the exact request type sent through the pipeline, which here is the command record. The inspected validators target the payload DTOs instead, so the per-field rules are not re-executed automatically when a command is dispatched. If you expose the API to direct clients, review whether a command-level validator or an explicit handler check is needed for each operation; the UI path alone does not prove server-side validation.

Map status codes and errors

The response helper returns 200 by default and 201 for created resources. The global exception middleware maps typed exceptions to status codes and writes them as the same ApiResponse envelope with IsSuccess = false and an errors list: validation failures to 400, missing resources to 404, duplicate records to 409, and permission or authentication problems to 403 and 401. In the inspected CRM scope the only handler that throws a duplicate-record exception is the sales-team name check, so a direct client that posts a duplicate team name receives a 409 conflict while the create lead endpoint trusts the caller.

Apply the design elsewhere

Compare the lead contract with the schema article to see how DTOs map to columns, and read CRM Vertical Slice Architecture to trace one request through the handler. If you add a client, reuse BaseService so token handling, refresh, and error snackbars stay consistent. Building a CRM Interface with Blazor and MudBlazor shows the consumer side of this contract. See this architecture implemented in a complete Blazor CRM application.