dotkernel-api-endpoint
Use when adding or changing an endpoint in a Dotkernel API project (or a project derived from it) — a new route, handler, request or response contract, resource loading by id, a caller-scoped /account
SKILL.md.claude/skills/dotkernel-api-endpoint/SKILL.md
GitHubAPI endpoint conventions
Applies to
Projects: - Dotkernel API
Packages: - none
API only. Confirm the variant with dotkernel-project-variants first. In a templated
project this skill does not apply — use dotkernel-handler-naming dialect B instead.
This skill is the checklist and the API-specific rules no other skill states. For everything else it points to the skill that owns the topic. When the repo differs from the stock skeleton, follow the repo and tell the user this skill needs updating.
Where each topic lives
| Concern | Skill |
|---|---|
| Where files go, ConfigProvider wiring, new module registration | dotkernel-module-structure |
| Handler class name, URL shape, route name, RoutesDelegator style | dotkernel-handler-naming |
| InputFilters, Inputs, sort/filter whitelisting | dotkernel-input-validation |
| HAL resources and collections, pagination, errors | dotkernel-responses |
| Authorization config, ownership, guest access | dotkernel-security |
OpenAPI.php operations |
dotkernel-openapi |
| Coverage matrix and test helpers | dotkernel-testing |
| Changing a live contract, deprecation | dotkernel-evolution-pattern |
| Scaffolding before hand-writing | dotkernel-dot-maker |
URLs and methods
- The caller's own resource lives under
account, never under an id:/user/account,/user/account/avatar. Its handlers live inHandler/Account/. - An action on an item is a trailing verb segment changed with
PATCH:PATCH /user/{id}/activate. - A process that creates no resource is a
POST:POST /user/account/recoversends an e-mail. - A one-time token from an e-mail link is a path placeholder:
/user/account/reset-password/{hash}. - A related list with no parent item is a fixed segment:
/user/role. - No
PUT. No version prefix by default — the rare exceptions, and how to propose one, are indotkernel-evolution-pattern.
The caller and the item in the path
- The caller is
$request->getAttribute(IdentityInterface::class). Never take the caller's id from the path, the query or the body. - Load the item in the path with
#[Resource(entity: X::class)]onhandle(). The resource provider middleware finds it by{id}, throwsNotFoundExceptionwhen it is missing orisDeleted(), and puts it on the request as$request->getAttribute(X::class). The handler does not repeat that check. identifier:andplaceholder:look an item up by another column.guard:takes an invokableResourceGuardInterfaceclass that refuses by throwing — one way to enforce ownership (seedotkernel-securityfor the alternatives).
Request and response shape
- Request and response fields are
camelCase:passwordConfirm,firstName. - An upload is validated from
$request->getUploadedFiles(), and its route gets its ownmultipart/form-dataentry inconfig/autoload/content-negotiation.global.php, keyed by route name. - Return through the
AbstractHandlerhelpers only:
| Helper | Status | Use |
|---|---|---|
createResponse($request, $entityOrCollection) |
200 | read, update |
createdResponse($request, $entity) |
201 | create |
noContentResponse() |
204 | delete |
infoResponse($message) |
200 | an action with no resource; body {"info": {"messages": [...]}} |
getArrayCopy()never exposes a password hash, token, one-time hash or internal numeric id. The stockUser::getArrayCopy()returns the activationhash— do not copy that into new entities.- An action that is already done (activating an active account) is a
ConflictException.
Lists
Paginator::getParams($params, '<default sort>') defaults limit to 10 but accepts any positive
value, and all returns up to 1,000 rows. Clamp limit to a documented maximum in the service,
next to the sort whitelist.
Authorization
- A request without a token runs as guest; a route not granted to guest answers 403, not 401.
- A new guest grant makes an endpoint public. State that decision explicitly in the merge request; never add it as a side effect.
OpenAPI
operationIdis the handler class name withoutHandler, in lowerCamelCase:getUserCollection,postUserResource,patchUserActivate.- Authenticated operations declare
security: [['AuthToken' => []]]; public ones omit it.
Tests beyond the standard matrix
On top of the coverage table in dotkernel-testing, cover:
- a caller without a token — 403 unless the route is granted to guest;
- another user's item, where a guard or ownership check applies;
- a conflict, where the endpoint can raise one.
Checklist for a new endpoint
- Path, method, handler class, route name and
operationIdfollow the naming rules. - Handler extends
AbstractHandler, takes dependencies through#[Inject], holds no business logic. - An item in the path is loaded with
#[Resource]; ownership enforced if it belongs to a user. - Input validated by an InputFilter of Input classes, catalogue messages, lengths matching the columns.
-
sortwhitelisted andlimitclamped in the service. - Response through a helper; entity or collection registered in the HAL
MetadataMap. -
getArrayCopy()exposes only the intended fields. - Errors thrown as typed exceptions with catalogue messages.
- Handler under
delegatorsandfactoriesin the moduleConfigProvider. - Route name granted in
authorization.global.phpto every role that needs it; to guest only by explicit decision. - A non-JSON request body has its content-negotiation entry.
- Operation in the module's
OpenAPI.php, with its failure responses. - Tests cover the matrix plus the cases above;
composer checkpasses.