scaffold-api-endpoint
Skill16 starsChanged 3 days agoApache-2.0View on GitHub
Generate a new REST API controller with fluent builder pattern, typed endpoints, permission-based auth, DTOs, and main.py registration. Use when user says "create API endpoint", "scaffold controller", "new REST endpoint", "add CRUD API", "generate API route", "build endpoint for X", or "add API controller". Do NOT use for service layer business logic (use scaffold-api-service), frontend SDK generation (use generate-sdk), or entity/repository scaffolding (use scaffold-api-repository).
We split this skill into the rules it gives an agent
16 rules. About 0 are common, 0 are distinctive, and 16 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
16
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
Read the API scope guide: packages/api/CLAUDE.md
- Very specific: line 154
Inherit from
Controller: Always extend swiss_ai_hub.core.routes.controller - Very specific: line 155
Fluent builder: Every endpoint method returns Self for chaining
- Very specific: line 157
Named-only constructor args: Use * to enforce keyword arguments
- Very specific: line 160
Permission templates: Use {path_param} placeholders for dynamic permission checks
- Very specific: line 161
Tags: Always pass tags=self.tags to router decorators
- Very specific: line 205
Add to all 4 locale files at packages/api/swiss_ai_hub/api/i18n/translations/api/controllers.{en,de,fr,it}.yml:
- Very specific: line 218
Create test directory and stub at packages/api/playground/testing/tests/<resource>/test_<resource>_api.py.
- Very specific: line 235
Locale handler: Always inject t: Annotated[LocaleHandler, Depends(use_locale)]
- Very specific: line 236
Pagination types: Use PageNumber and PageSize type aliases from swiss_ai_hub.api.pagination.type
- Very specific: line 238
Path validation: Use Path(pattern=r"^[a-f0-9]{24}$") for MongoDB ObjectId params
- Very specific: line 254
404 on new endpoint: Verify the controller is mounted in main.py and the fluent builder methods are chained
- Very specific: line 255
Permission denied (403): Check the permission template string matches what is configured in the role system (e.g., aihub.user.?> vs …
- Very specific: line 257
Missing tags in Swagger: Ensure tags=self.tags is passed to every router decorator
- Very specific: line 258
i18n key not found: Verify locale YAML files have the correct nested path under api.controllers.<resource>
- Very specific: line 259
Duplicate route conflict: Check that route parameter default values do not clash with existing controllers
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Scaffold a New REST API Controller
Generate a new controller with endpoints. The resource name should be provided via $ARGUMENTS.
Step 1: Read Reference Materials
- Read the API scope guide:
packages/api/CLAUDE.md - Study these reference controllers:
- CRUD:
packages/api/swiss_ai_hub/api/routes/agent/agent_controller.py - Simple:
packages/api/swiss_ai_hub/api/routes/role/role_controller.py - Complex:
packages/api/swiss_ai_hub/api/routes/thread/thread_controller.py - Base class:
packages/core/swiss_ai_hub/core/routes/controller.py - Registration:
packages/api/app/main.py
- CRUD:
- Extract the resource name from
$ARGUMENTSand derivesnake_case(dirs/files) andCamelCase(classes)
Step 2: Create Directory Structure
packages/api/swiss_ai_hub/api/routes/<resource>/
├── __init__.py
├── <resource>_controller.py
├── <resource>_service.py
└── dto/
├── __init__.py
├── <resource>_dto.py
├── create_<resource>_request.py
├── update_<resource>_request.py
└── paginated_<resource>s_response.pyStep 3: Create the Controller
File: packages/api/swiss_ai_hub/api/routes/<resource>/<resource>_controller.py
from typing import Annotated, Self
from swiss_ai_hub.core.auth.dependencies.auth_handler import AuthHandler
from swiss_ai_hub.core.auth.identity.user_identity import UserIdentity
from swiss_ai_hub.core.i18n.locale_handler import LocaleHandler
from swiss_ai_hub.core.routes.controller import Controller
from fastapi import Depends, Response, Security, status
from swiss_ai_hub.api.i18n.api_locale_string import ApiLocaleString
from swiss_ai_hub.api.i18n.dependencies.use_locale import use_locale
from swiss_ai_hub.api.pagination.type.page_number import PageNumber
from swiss_ai_hub.api.pagination.type.page_size import PageSize
from swiss_ai_hub.api.routes.<resource>.dto.create_<resource>_request import Create<Resource>Request
from swiss_ai_hub.api.routes.<resource>.dto.<resource>_dto import <Resource>DTO
from swiss_ai_hub.api.routes.<resource>.dto.paginated_<resource>s_response import Paginated<Resource>sResponse
from swiss_ai_hub.api.routes.<resource>.dto.update_<resource>_request import Update<Resource>Request
from swiss_ai_hub.api.routes.<resource>.<resource>_service import <Resource>Service
class <Resource>Controller(Controller):
"""Controller for <resource> management."""
name = ApiLocaleString.from_i18n_path("api.controllers.<resource>.name")
description = ApiLocaleString.from_i18n_path("api.controllers.<resource>.description")
icon = "mage:icon-name"
def __init__(
self,
*,
auth: AuthHandler,
route: str = "/<resource>s",
additionally_required_permission: str | None = None,
):
super().__init__(auth=auth, route=route, additionally_required_permission=additionally_required_permission)
# ==================== LIST ====================
def get_<resource>s(self, route: str = "/") -> Self:
@self.router.get(route, tags=self.tags)
async def get_<resource>s(
user: Annotated[UserIdentity, Security(self.user_with_permission("aihub.user.?>"))],
t: Annotated[LocaleHandler, Depends(use_locale)],
page: PageNumber = 1,
page_size: PageSize = 20,
) -> Paginated<Resource>sResponse:
"""Retrieve a paginated list of <resource>s."""
total, items = await <Resource>Service.get_paginated_<resource>s(
user_id=user.id, t=t, page=page, page_size=page_size,
)
total_pages = (total + page_size - 1) // page_size
return Paginated<Resource>sResponse(
<resource>s=items, total=total, page=page,
page_size=page_size, total_pages=total_pages,
)
return self
# ==================== GET ====================
def get_<resource>(self, route: str = "/{<resource>_id}") -> Self:
@self.router.get(route, tags=self.tags)
async def get_<resource>(
<resource>_id: str,
_: Annotated[UserIdentity, Security(self.user_with_permission("aihub.user.<resource>.{<resource>_id}"))],
t: Annotated[LocaleHandler, Depends(use_locale)],
) -> <Resource>DTO:
"""Retrieve details for a specific <resource>."""
return await <Resource>Service.get_<resource>_by_id(<resource>_id, t)
return self
# ==================== CREATE ====================
def create_<resource>(self, route: str = "/") -> Self:
@self.router.post(route, tags=self.tags, status_code=status.HTTP_201_CREATED)
async def create_<resource>(
request: Create<Resource>Request,
user: Annotated[UserIdentity, Security(self.user_with_permission("aihub.admin.<resource>"))],
t: Annotated[LocaleHandler, Depends(use_locale)],
) -> <Resource>DTO:
"""Create a new <resource>."""
return await <Resource>Service.create_<resource>(request, user, t)
return self
# ==================== UPDATE ====================
def update_<resource>(self, route: str = "/{<resource>_id}") -> Self:
@self.router.put(route, tags=self.tags)
async def update_<resource>(
<resource>_id: str,
request: Update<Resource>Request,
_: Annotated[UserIdentity, Security(self.user_with_permission("aihub.admin.<resource>.{<resource>_id}"))],
t: Annotated[LocaleHandler, Depends(use_locale)],
) -> <Resource>DTO:
"""Update an existing <resource>."""
return await <Resource>Service.update_<resource>(<resource>_id, request, t)
return self
# ==================== DELETE ====================
def delete_<resource>(self, route: str = "/{<resource>_id}") -> Self:
@self.router.delete(route, tags=self.tags, status_code=status.HTTP_204_NO_CONTENT)
async def delete_<resource>(
<resource>_id: str,
_: Annotated[UserIdentity, Security(self.user_with_permission("aihub.admin.<resource>.{<resource>_id}"))],
) -> Response:
"""Delete a <resource>."""
await <Resource>Service.delete_<resource>(<resource>_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
return selfController Architecture Rules
- Inherit from
Controller: Always extendswiss_ai_hub.core.routes.controller - Fluent builder: Every endpoint method returns
Selffor chaining - Metadata: Set
name(ApiLocaleString),description,icon(Iconify) - Named-only constructor args: Use
*to enforce keyword arguments - Default route: Set in constructor
route: str = "/<resource>s" - Inner function pattern: Endpoint function is defined inside the method
- Permission templates: Use
{path_param}placeholders for dynamic permission checks - Tags: Always pass
tags=self.tagsto router decorators - Status codes: 201 for create, 204 for delete, 200 (default) for get/update
Permission Patterns
| Access Level | Template | Usage |
| Any user | "aihub.user.?>" | List own resources |
| Specific resource | "aihub.user.<resource>.{<resource>_id}" | View specific resource |
| Admin-only | "aihub.admin.<resource>" | Create resources |
| Admin + resource | "aihub.admin.<resource>.{<resource>_id}" | Update/delete |
| Service admin | f"aihub.admin.service.{self.service_name}" | Manage entire service |
User Identity Usage
# When you need the identity (e.g., filter by user):
user: Annotated[UserIdentity, Security(self.user_with_permission("aihub.user.?>"))]
# Access: user.id, user.name, user.email, user.roles
# When you only need permission check:
_: Annotated[UserIdentity, Security(self.user_with_permission("aihub.admin.<resource>"))]Step 4: Register in main.py
Edit packages/api/app/main.py:
from swiss_ai_hub.api.routes.<resource>.<resource>_controller import <Resource>Controller
runner.mount(
# ... existing controllers ...
<Resource>Controller(auth=auth)
.get_<resource>s()
.get_<resource>()
.create_<resource>()
.update_<resource>()
.delete_<resource>(),
)Step 5: Add i18n Keys
Add to all 4 locale files at packages/api/swiss_ai_hub/api/i18n/translations/api/controllers.{en,de,fr,it}.yml:
<resource>:
name: "<Resource>s"
description: "Manage <resource>s"The api.controllers. prefix in ApiLocaleString.from_i18n_path("api.controllers.<resource>.name") resolves to the
file path translations/api/controllers.{locale}.yml — only the <resource>.name part is a key inside the YAML.
Step 6: Scaffold Tests
Create test directory and stub at packages/api/playground/testing/tests/<resource>/test_<resource>_api.py. Follow the
patterns in playground/testing/tests/agent/ or playground/testing/tests/role/.
Step 7: Verify
- Confirm the controller is importable:
cd packages/api && uv run python -c "from swiss_ai_hub.api.routes.<resource>.<resource>_controller import <Resource>Controller" - Confirm registration in
packages/api/app/main.py— the controller must be imported and mounted viarunner.mount() - Confirm i18n keys exist in all 4 locale files:
packages/api/swiss_ai_hub/api/i18n/translations/api/controllers.{en,de,fr,it}.yml - Run tests:
cd packages/api && make test
Key Conventions
- Controller is thin: Delegates to Service, only handles HTTP concerns
Annotatedfor everything: Params, dependencies, auth — all useAnnotated- Docstrings on endpoints: Short description of what the endpoint does
- Locale handler: Always inject
t: Annotated[LocaleHandler, Depends(use_locale)] - Pagination types: Use
PageNumberandPageSizetype aliases fromswiss_ai_hub.api.pagination.type - Error handling: Let services raise
HTTPException-- don't catch in controllers - Path validation: Use
Path(pattern=r"^[a-f0-9]{24}$")for MongoDB ObjectId params
Examples
Input: $ARGUMENTS = "project" Expected output files:
packages/api/swiss_ai_hub/api/routes/project/project_controller.pywithProjectController(Controller)packages/api/swiss_ai_hub/api/routes/project/project_service.py(stub -- use/scaffold-api-servicefor full service)packages/api/swiss_ai_hub/api/routes/project/dto/project_dto.py,create_project_request.py,update_project_request.py,paginated_projects_response.py- Registration added to
packages/api/app/main.py - i18n keys added to
packages/api/swiss_ai_hub/api/i18n/translations/api/controllers.{en,de,fr,it}.yml
Troubleshooting
- 404 on new endpoint: Verify the controller is mounted in
main.pyand the fluent builder methods are chained - Permission denied (403): Check the permission template string matches what is configured in the role system (e.g.,
aihub.user.?>vsaihub.admin.resource) - Missing tags in Swagger: Ensure
tags=self.tagsis passed to every router decorator - i18n key not found: Verify locale YAML files have the correct nested path under
api.controllers.<resource> - Duplicate route conflict: Check that
routeparameter default values do not clash with existing controllers
In this repository50 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- .github/actions/CLAUDE.mdCLAUDE.md
- arc42Skill
- audit-frontendSkill
- bot-frameworkSkill
- create-or-audit-claude-mdSkill
- create-or-audit-skillSkill
- create-or-audit-subagentSkill
- create-prSkill
- dagster-pipelinesSkill
- debug-agentSkill
- debug-frontendSkill
- debug-pipelineSkill
- design-systemSkill
- docker-devSkill
- document-decisionSkill
- document-featureSkill
- explainSkill
- generate-sdkSkill
- i18nSkill
- implement-feedback-from-prSkill
- likec4-dslSkill
- lintSkill
- merge-mainSkill
- nats-eventsSkill
- plan-issueSkill
- pr-demo-videoSkill
- primevue-lookupSkill
- rclone-guideSkill
- reflectSkill
- review-diffSkill
- scaffold-agentSkill
- scaffold-api-repositorySkill
- scaffold-api-serviceSkill
- scaffold-bot-handlerSkill
- scaffold-composableSkill
- scaffold-event-displaySkill
- scaffold-frontend-componentSkill
- scaffold-frontend-pageSkill
- scaffold-frontend-subpageSkill
- scaffold-pipelineSkill
- setup-bot-connectionSkill
- splice-issueSkill
- sync-agent-docsSkill
- test-scopeSkill
- update-docSkill
- verify-sdk-syncSkill
- write-docSkill
- write-issueSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

