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:
| Endpoint | Method | Purpose | Response value |
|---|---|---|---|
| /api/lead | GET | Grid rows for the data table | List of GetLeadListResponse |
| /api/lead/{id} | GET | Single lead including audit fields | GetLeadByIdResponse |
| /api/lead | POST | Create a lead | CreateLeadResponse (Id, code) |
| /api/lead/update | POST | Update a lead | UpdateLeadResponse |
| /api/lead/delete/{id} | POST | Delete (soft delete) a lead | Success flag |
| /api/lead/lookup | GET | Lookup options for forms | LeadLookupResponse |
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.