Extending DiagramaHub¶
Recipes for the most common extensions. In all of them: text in Spanish and English, tests for every new endpoint and no business logic in routes.
Creating a backend module¶
One entity = one folder. Example with a templates entity:
- Create the folder
backend/app/api/v1/templates/with__init__.py,interfaces.py,repository.py,services.py,schemas.pyandroutes.py. - Schemas (
schemas.py): Pydantic v2 modelsTemplateBase,TemplateCreate,TemplateUpdate,TemplateResponseand the Beanie documentTemplateInDB(Document). - Interface (
interfaces.py):class ITemplateRepository(ABC)with@abstractmethodmethods (create,get_by_id,list_by_user, …). - Repository (
repository.py):TemplateRepository(ITemplateRepository)implemented with Beanie. - Service (
services.py):TemplateServicereceives anITemplateRepositoryin its constructor; the rules live here (resource ownership, limits, validation). - Routes (
routes.py): anAPIRouter, aget_template_service()function that builds the service with the concrete repository, and endpoints that useDepends(get_current_user_id)and delegate to the service. - Register the document in
init_beanie(document_models=[...])and the router withapp.include_router(router, prefix=settings.API_V1_PREFIX)inmain.py. - Tests in
backend/tests/api/v1/templates/usingauthenticated_client.
from fastapi import APIRouter, Depends
from app.api.deps import get_current_user_id
from app.api.v1.templates.repository import TemplateRepository
from app.api.v1.templates.schemas import TemplateCreate, TemplateResponse
from app.api.v1.templates.services import TemplateService
router = APIRouter(prefix="/templates", tags=["templates"])
def get_template_service() -> TemplateService:
"""Build the service with its concrete repository."""
return TemplateService(repository=TemplateRepository())
@router.post("", response_model=TemplateResponse, status_code=201)
async def create_template(
body: TemplateCreate,
user_id: str = Depends(get_current_user_id),
service: TemplateService = Depends(get_template_service),
) -> TemplateResponse:
"""Create a template owned by the current user."""
return await service.create(body, user_id)
Every private route must identify the user
Each endpoint that reads or changes a user's data needs Depends(get_current_user_id) (or get_current_user), and the service must check that the resource belongs to that user. Without it, anyone who knows an id could read it.
Adding an AI provider¶
- Client: create
backend/app/api/v1/ai_providers/clients/<provider>_client.pywith a class that extendsBaseAIClientand implements its abstract methods (complete,generate_description,fix_diagram,validate_api_key,chat_with_context,summarize_conversationand theprovider_nameproperty). Diagram generation and improvement already live inBaseAIClient, with the same prompt for every provider. - Enum: add the value to
AIProviderTypeinai_providers/schemas.pyand infrontend/src/types/ai.ts. - Factory: register it in
_clients_mapinai_providers/clients/factory.py. -
Model catalog: add its models to
model_catalog.json, the single source of truth:{"provider": "new", "id": "new-model-1", "context_window": 128000, "listed": true, "recommended": true, "supports_temperature": true}listed: the model is offered in the UI (unlisted ones are kept so saved configurations keep working).recommended: exactly one per provider; it is the default model.context_window: used by chat compaction.supports_temperature:falsewhen the model rejects that parameter.
Check the models against the provider's docs and with a live call.
-
Frontend: add the same
listedmodels toAI_PROVIDER_MODELSinfrontend/src/types/ai.ts. TheaiModelCatalog.test.tstest fails if they differ (ids, order and recommended). - Text: provider name and help under
ai.providers.<id>andai.help.<id>ines.jsonanden.json. The providers modal iterates overAIProviderType, so it shows up on its own. - Measure: run the AI benchmark with the new model.
Adding a frontend page¶
- Create the component in
frontend/src/pages/MyPage.tsx. - Add the route in
App.tsx: insidePrivateRouteandSidebarLayoutif it needs a session, or next to the public ones if not. - Add the API methods to
ApiServiceinservices/api.tsand the types intypes/. - Follow
DESIGN.md: Tailwind, dark mode, accessibility, no UI libraries. - All text through
t().
Adding UI text (i18n)¶
- Add the key to both
frontend/src/i18n/locales/es.jsonanden.json, in the matching section (common,nav,auth,diagram,editor,errors, …). - Use the same
{{name}}placeholders in both languages. For plurals use i18next's forms (_one/_other), not the legacy_pluralsuffix. - Run
pnpm test:i18nParity.test.tschecks keys, placeholders and plurals.