name: add-endpoint
description: Add an HTTP endpoint to an existing module — updates the audience-scoped controller (creates one if needed), DTO, service method, and i18n keys for any new error messages. Triggers: "add an endpoint", "expose endpoint X on module Y", "add a route to ", "/add-endpoint".
user-invocable: true
argument-hint: [--audience admin|user|public] [--name ]
allowed-tools: Read, Grep, Glob, Write, Edit, Bash
Add Endpoint
Add a new endpoint to an existing module without breaking surrounding conventions.
Arguments
<module>— plural folder undersrc/modules/(kebab-case,cities,users). Singular form is rewritten to plural.<METHOD>—GET | POST | PATCH | PUT | DELETE.<path>— full route or suffix appended to the controller base.:idplaceholders honored.--audience <kind>— target controller. If omitted, infer from path (admin/...→ admin).--name <handlerName>— handler method name. Default: derived from method+path (e.g.GET /:id→findOne,POST /→create).
Phase 1 — Locate the target controller
Glob src/modules/<module>/— confirm it exists.- Read
controllers/. Match the audience:- Matching
<audience>-<module>.controller.tsexists → reuse it. - Otherwise, ask once (or just create if
--audiencewas passed).
- Matching
- Read one sibling controller for decorator/order/style.
Phase 2 — Plan inputs and outputs
| Method | Handler | Service call | Body | Response |
|---|---|---|---|---|
POST / |
create / domain verb (book) |
createFor<Audience> / bookByAdmin |
yes | <Feature>ResponseDto + @HttpCode(CREATED) |
POST /:id/<verb> |
<verb> |
<verb>By<Audience> / <verb>For<Audience> |
sometimes | <Feature>ResponseDto |
GET / |
findAll |
findPageFor<Audience> |
no | IPaginatedResponse<<Feature>ResponseDto> via mapPaginated |
GET /:id |
findOne |
findByIdOrFail / findOwnedByXOrFail |
no | <Feature>ResponseDto |
PATCH /:id |
update |
updateFor<Audience> |
yes | <Feature>ResponseDto |
PATCH /:id/<verb> |
<verb> / update<Verb> |
<verb>For<Audience> |
yes | <Feature>ResponseDto |
DELETE /:id |
remove |
deleteFor<Audience> |
no | void + @HttpCode(NO_CONTENT) |
Handler name never carries the audience (no adminList, publicGetById) — audience lives in the controller class name and URL prefix. Service methods do carry the audience. See @../naming-conventions/SKILL.md.
If a body is needed:
- New action → new DTO file
<verb>-<feature>.dto.ts. - Tightening existing input → extend the existing DTO.
- Use
class-validator+Translator.trValMsg('common.validation.*'). List query DTOs extendPaginationQueryDtofromcore/pagination/dto/.
Phase 3 — Update the service
Open the matching service (or facade). Add the new method:
- Name:
<verb>By<Audience>for role-scoped action; otherwise intent-based. - Accept primitive IDs + DTO. Never accept the raw Express request.
- Throw
NotFoundException/ForbiddenException/ConflictExceptionwiththis.translator.tr('<feature>.errors.<key>')(add the key in Phase 5 if missing). - Paginated lists: call
this.<feature>Repository.findPageFor<Audience>(...)and return the envelope. - If the service is split (booking/query/status), add to the matching sub-service and expose through the facade.
Phase 4 — Update the controller
@Get()
async findAll(
@Query() query: List<Plural>AdminQueryDto,
): Promise<IPaginatedResponse<<Feature>ResponseDto>> {
const result = await this.<feature>Service.findPageForAdmin(query);
return mapPaginated(result, <Feature>ResponseDto.fromEntity);
}
Rules:
@Param('id', PositiveIntPipe) id: numberfor ID params.@CurrentUser() <role>: <Role>Entityonly when the audience needs the caller (user scope checks).@HttpCode(HttpStatus.CREATED)on POST creates.- Never
@Req()/@Res()— rely on decorators. - Imports use path aliases.
New controller file (if Phase 1 chose to create one):
- Name:
<audience>-<feature>.controller.ts(feature is singular). - Class decorators:
@Protected(Role.<AUDIENCE>),@Controller({ path: '<audience>/<plural>', version: '1' }). - Add to
controllers:in<plural>.module.ts.
Phase 5 — i18n keys
For every new this.translator.tr('<feature>.errors.<key>'):
- Add to both
infrastructure/i18n/translations/{ar,en}/<feature>.json(mirrored). - Run
npm run i18n:sync.
Prefer existing common.validation.* keys over inventing feature-scoped ones.
Phase 6 — Verify
npm run i18n:syncnpm run lintnpm run build
Phase 7 — Confirm
✓ Added <METHOD> <full route> → <Service>.<method>
✓ Controller: <path-to-controller>
✓ DTO: <path-to-dto> (or "reused <existing>")
✓ i18n keys added: <feature>.errors.<key> (×N)
✓ lint / build / i18n green
Postman:
- Run /generate-postman-collection <feature> to refresh the collection.
Anti-patterns
- Routes for another entity inside
modules/admin/— put them inmodules/<that-entity>/controllers/admin-<entity>.controller.ts. - Reading another module's repository directly. Go through its service.
- Bypassing DTOs by reading
req.body. - Manually throwing English strings without
Translator.tr(...). Loggerortry/catchnot required by a constraint-translation case.