From dknet-minimal
Create Minimal API endpoint configurations using this project's IEndpointConfig pattern with fluent helpers (MapGetList, MapGetById, MapPost, MapPut, MapDelete). Use after AppServices actions are ready.
npx claudepluginhub baoduy/dknet.templates --plugin dknet-minimalThis skill uses the workspace's default tool permissions.
Create versioned REST API endpoints that wire AppServices actions to HTTP routes using the project's fluent endpoint mapping pattern.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
Create versioned REST API endpoints that wire AppServices actions to HTTP routes using the project's fluent endpoint mapping pattern.
1)/customer-profiles)IEndpointConfig interface — auto-discovered via assembly scanninginternal sealedVersion → API version integerGroupEndpoint → route path (e.g., /customer-profiles)Map(RouteGroupBuilder group) → wire endpoints using fluent helpersTag is auto-derived from GroupEndpoint (strips / → kebab-case)UseEndpointConfigs() scans the assembly for all IEndpointConfig implementations, creates versioned groups with:
SetUserIdPropertyFilter → fills RequestBase.ByUser from JWTFluentValidation auto-validation (if configured)RequireAuthorization() (if auth is configured){version:apiVersion} path segment| Helper | HTTP | Request Interface | Response |
|---|---|---|---|
MapGetList<TEntity, TDto>() | GET / | Auto-wires GenericListParameters (filter/sort/page) — NO request type needed | PagedResponse<TDto> |
MapGetById<TEntity, TDto>() | GET /{id:guid} | Auto-wires Guid id from route — NO request type needed | TDto |
MapPost<TReq, TDto>() | POST / | Fluents.Requests.IWitResponse<TDto> | 201 + TDto |
MapPut<TReq, TDto>() | PUT / | Fluents.Requests.IWitResponse<TDto> | 200 + TDto |
MapDelete<TReq>() | DELETE / | Fluents.Requests.INoResponse | 200 |
MapDelete<TReq, TDto>() | DELETE / | Fluents.Requests.IWitResponse<TDto> | 200 + TDto |
MapGet<TReq, TDto>() | GET / | Fluents.Queries.IWitResponse<TDto> | 200 + TDto |
MapGetPage<TReq, TDto>() | GET / | Fluents.Queries.IWitPageResponse<TDto> | 200 + PagedResponse<TDto> |
MapPatch<TReq, TDto>() | PATCH / | Fluents.Requests.IWitResponse<TDto> | 200 + TDto |
MapGetStatusCounts<TEntity>() | GET /status | GenericStatusCountsParameters | List<StatusCountsResult> |
All fluent helpers automatically:
IMessageBus.Send(request) (SlimMessageBus)typeof(TCommand).Name.Contains("Create"))MapGetList and MapGetById are different — they wire directly to IRepositorySpec with generic specs, NOT through the message bus. They don't need a request type parameter.src/ApiEndpoints/Minimal.Api/
└── ApiEndpoints/
└── {Entity}V{N}Endpoint.cs ← One file per entity per version
Create src/ApiEndpoints/Minimal.Api/ApiEndpoints/{Entity}V1Endpoint.cs:
using Minimal.AppServices.{Feature}.V1.Actions;
using Minimal.Domains.Features.{Feature}.Entities;
using {Entity}Dto = Minimal.AppServices.{Feature}.V1.{Entity}Dto;
namespace Minimal.Api.ApiEndpoints;
internal sealed class {Entity}V1Endpoint : IEndpointConfig
{
#region Properties
public int Version => 1;
public string GroupEndpoint => "/{kebab-case-plural}";
#endregion
#region Methods
public void Map(RouteGroupBuilder group)
{
// GET /v1/{route} — paginated list with filtering/sorting
group.MapGetList<{Entity}, {Entity}Dto>()
.WithDescription("Get all {entities}");
// GET /v1/{route}/{id} — single entity by ID
group.MapGetById<{Entity}, {Entity}Dto>()
.WithDescription("Get {entity} by id");
// POST /v1/{route} — create new entity
group.MapPost<Create{Entity}Request, {Entity}Dto>()
.WithDescription("Create {entity}");
// PUT /v1/{route} — update existing entity
group.MapPut<Update{Entity}Request, {Entity}Dto>()
.WithDescription("Update {entity} by id");
// DELETE /v1/{route} — soft-delete entity
group.MapDelete<Delete{Entity}Request>()
.WithDescription("Delete {entity} by id");
}
#endregion
}
using Minimal.Api.Configs.Idempotency;
// In the Map method:
group.MapPost<Create{Entity}Request, {Entity}Dto>()
.AddIdempotencyFilter()
.WithDescription(
"Create {entity}. <br/><br/> Note: Idempotency key is required in the header. <br/>" +
"X-Idempotency-Key: {IdempotencyKey} <br/>");
For endpoints beyond basic CRUD:
// Custom query endpoint
group.MapGet<CustomQueryRequest, CustomResponseDto>("/custom-route")
.WithDescription("Custom query description");
// Status counts endpoint
group.MapGetStatusCounts<{Entity}>("status",
new StatusPropertyInfo("Status", typeof({Entity})))
.WithDescription("Get {entity} status counts");
internal sealed class {Entity}V1Endpoint : IEndpointConfig
{
public int Version => 1;
public string GroupEndpoint => "/{route}";
public string? AuthPolicy => "AdminOnly"; // Override default auth
public string Tag => "Custom Tag"; // Override auto-derived tag
public void Map(RouteGroupBuilder group) { /* ... */ }
}
using Minimal.Api.Configs.Idempotency;
using Minimal.AppServices.CustomerProfiles.V1.Actions;
using Minimal.Domains.Features.Profiles.Entities;
using CustomerProfileDto = Minimal.AppServices.CustomerProfiles.V1.CustomerProfileDto;
namespace Minimal.Api.ApiEndpoints;
internal sealed class CustomerProfileV1Endpoint : IEndpointConfig
{
public int Version => 1;
public string GroupEndpoint => "/customer-profiles";
public void Map(RouteGroupBuilder group)
{
group.MapGetList<CustomerProfile, CustomerProfileDto>()
.WithDescription("Get all profiles");
group.MapGetById<CustomerProfile, CustomerProfileDto>()
.WithDescription("Get profile by id");
group.MapPost<CreateProfileRequest, CustomerProfileDto>()
.AddIdempotencyFilter()
.WithDescription(
"Create profile. <br/><br/> Note: Idempotency key is required in the header. <br/>" +
"X-Idempotency-Key: {IdempotencyKey} <br/>");
group.MapPut<UpdateProfileRequest, CustomerProfileDto>()
.WithDescription("Update profile by id");
group.MapDelete<DeleteProfileRequest>()
.WithDescription("Delete profile by id");
}
}
IEndpointConfig interfaceinternal sealedVersion returns correct API version integerGroupEndpoint uses kebab-case with leading /Map() uses fluent helpers (MapGetList, MapGetById, MapPost, MapPut, MapDelete)using {Entity}Dto = ....WithDescription() for OpenAPI docsMapPost (auto 201 for "Create" in name)MapDelete<TReq>() (single generic param).AddIdempotencyFilter())Minimal.Api/ApiEndpoints/dotnet build src/DKNet.Templates.sln -c Release passes| Mistake | Fix |
|---|---|
Creating manual app.MapGet(...) routes | Use fluent helpers — they wire bus dispatch + error responses |
| Forgetting DTO type alias | Add using {Entity}Dto = Minimal.AppServices.{Feature}.V1.{Entity}Dto; |
Using MapDelete<TReq, TDto> when no response needed | Use single-param MapDelete<TReq>() for void deletes |
Making endpoint class public | Must be internal sealed — discovered by assembly scanning |
Wrong GroupEndpoint format | Must start with /, use kebab-case plural (e.g., /order-items) |
Registering endpoint in Program.cs | NOT needed — UseEndpointConfigs() auto-discovers all IEndpointConfig |
After creating the endpoint, verify the full vertical slice works:
# Build
dotnet build src/DKNet.Templates.sln -c Release
# Run API
dotnet run --project src/ApiEndpoints/Minimal.Api
# Test via Scalar UI (default: https://localhost:5001/scalar)
# Or test via curl:
curl -X GET https://localhost:5001/api/v1/{route}
curl -X POST https://localhost:5001/api/v1/{route} -H "Content-Type: application/json" -d '{...}'