Standards for building and maintaining a robust Elysia + Bun backend with clean architecture, SOLID principles, strict file responsibility, Redis abstractions, and production-grade engineering practices.
Resources
12Install
npx skillscat add sebguevara/minuto90-backend Install via the SkillsCat registry.
name: minuto90-backend
description: Standards for building and maintaining a robust Elysia + Bun backend with clean architecture, SOLID principles, strict file responsibility, Redis abstractions, and production-grade engineering practices.
minuto90-backend
This skill defines the architectural and implementation standards for a backend built with Elysia + Bun.
The objective is to keep the backend:
- maintainable
- modular
- testable
- scalable
- observable
- secure
- easy to evolve over time
This skill must favor clarity over speed, structure over improvisation, and explicit boundaries over convenience shortcuts.
When to use
Use this skill when the task involves any of the following:
- creating or modifying an Elysia + Bun backend
- designing backend modules or domain boundaries
- implementing routes, use cases, repositories, workers, or infrastructure adapters
- integrating Redis, PostgreSQL, queues, sessions, rate limiting, caching, or background jobs
- refactoring code toward clean architecture
- reviewing backend structure, robustness, maintainability, or code quality
- enforcing file organization, naming conventions, and SOLID rules
- designing cross-cutting concerns such as logging, config, error handling, metrics, tracing, or security
Architectural goal
The backend must be organized by business module first, and by technical layer second.
Do not organize the codebase primarily by framework pieces like controllers, services, routes, repositories, or utils at the root level.
The main unit of organization must be the module or bounded context.
Canonical project structure
Use this structure as the default target:
src/
modules/
notifications/
domain/
application/
infrastructure/
presentation/
sports/
football/
domain/
application/
infrastructure/
presentation/
nba/
domain/
application/
infrastructure/
presentation/
shared/
domain/
application/
infrastructure/
stats/
domain/
application/
infrastructure/
presentation/
shared/
domain/
application/
infrastructure/
presentation/
bootstrap/
config/
workers/
server.tsIf a sub-area such as football, nba, push, or whatsapp is not a real module but only a technical variation, provider, or delivery channel, then it must not be promoted to top-level module status. In that case, keep it inside the relevant infrastructure or provider layer.
Module design rule
Before creating folders, determine what the thing actually is.
Every area must be classified as one of these:
- a business module
- a submodule of a business module
- a technical adapter
- a shared cross-cutting component
Do not mix those concepts in the same level of the tree.
For example:
notificationscan be a business modulepushandwhatsappare usually delivery channels or infrastructure adaptersfootballandnbamay be modules only if they truly have distinct rules and workflows- otherwise they should remain providers, strategies, or subdomains inside
sports
Clean architecture boundaries
Each module should respect these layers.
Domain
The domain layer contains pure business concepts and rules.
Allowed:
- entities
- value objects
- domain services
- domain errors
- repository contracts
- business rules and invariants
Not allowed:
- Elysia
- Redis clients
- database clients
- HTTP request/response logic
- environment variable access
- external SDKs
Application
The application layer contains use cases and orchestration.
Allowed:
- use cases
- application services when they coordinate use cases
- input/output DTOs
- command and query handlers
- transaction orchestration abstractions
- ports and contracts for infrastructure dependencies
Not allowed:
- inline SQL
- Redis client usage
- HTTP framework logic
- Elysia context
- transport-specific details
Infrastructure
The infrastructure layer contains implementations of external dependencies.
Allowed:
- repository implementations
- Redis adapters
- PostgreSQL adapters
- queue producers/consumers
- external API clients
- token signers
- mailers
- loggers
- cache implementations
This layer may depend on domain and application. Domain and application must not depend on infrastructure.
Presentation
The presentation layer contains transport-specific adapters.
Allowed:
- Elysia routes
- request parsing
- schema binding
- response mapping
- auth guards
- HTTP error translation
Presentation must call the application layer. It must not contain business logic.
File responsibility rules
One file, one primary responsibility
This is mandatory.
Each file should have one clear reason to change.
Examples:
create-user.use-case.ts
create-user.dto.ts
user.entity.ts
user.repository.ts
postgres-user.repository.ts
login.route.ts
login.schema.ts
auth-response.mapper.ts
build-session-cache-key.tsDo not place helper functions inside unrelated files just because they are only used once.
If a function has a distinct responsibility, create a dedicated file for it.
Typical extractions that deserve their own file:
- mappers
- schema definitions
- cache key builders
- normalizers
- policy checks
- formatters
- repository queries
- token parsers
- permission evaluators
Forbidden dumping grounds
Do not create or grow generic files such as:
utils.tshelpers.tscommon.tsmisc.tsservice.tswhen it contains many unrelated behaviorslib/as a vague catch-all folder
If a lib directory already exists, it must be reduced or eliminated unless every file inside it has a narrow, explicit, and stable purpose.
Naming conventions
Names must be explicit and role-based.
Prefer:
create-match.use-case.tsfind-user-by-email.repository.tsredis-session.store.tspush-notification.sender.tsfootball-score.provider.tslogin.schema.tsmatch-response.mapper.ts
Avoid vague names such as:
service.tsmanager.tshandler.tsunless it is specifically a handler abstractionfunctions.tstool.tsutil.ts
Folder conventions inside a module
Use a predictable structure inside each module.
Recommended pattern:
module-name/
domain/
entities/
value-objects/
errors/
services/
repositories/
application/
use-cases/
contracts/
dto/
mappers/
infrastructure/
persistence/
redis/
providers/
queue/
presentation/
http/
routes/
schemas/
mappers/Only create subfolders when they add clarity. Do not create empty architecture theater.
DTO placement rule
DTOs must not float randomly at the module root.
Place DTOs in:
application/dtowhen they belong to use case input/output boundariespresentation/http/schemaswhen they represent transport validation
Do not create root-level dtos/ in one module while other modules follow a different pattern.
SOLID rules
Single Responsibility Principle
Every class, function, and file must have one primary responsibility.
A route defines routes.
A use case implements one use case.
A repository persists and retrieves data.
A mapper maps.
A validator validates.
A cache key builder only builds keys.
Open/Closed Principle
Stable logic should be extendable without rewriting the core flow.
Use:
- interfaces
- strategies
- providers
- adapters
- composition
Avoid large if/else or switch chains for provider-specific behavior when a polymorphic solution is cleaner.
Liskov Substitution Principle
Implementations of a contract must behave consistently.
For example:
- all
SessionStoreimplementations must obey the same session semantics - all
CacheServiceimplementations must respect TTL expectations - all repository implementations must return equivalent domain concepts
Interface Segregation Principle
Interfaces must stay small and specific.
Avoid oversized contracts such as:
interface NotificationService {
sendPush(): Promise<void>
sendWhatsapp(): Promise<void>
saveNotification(): Promise<void>
retryFailedNotifications(): Promise<void>
cacheDeliveryStatus(): Promise<void>
}Prefer narrow contracts such as:
NotificationRepositoryNotificationSenderDeliveryStatusStoreNotificationQueue
Dependency Inversion Principle
High-level code must depend on abstractions, not concrete framework or infrastructure code.
Use cases may depend on contracts like:
UserRepositorySessionStoreCacheServiceScoreProviderNotificationSender
Use cases must not import:
- Elysia context
- raw Redis clients
- Prisma or SQL code directly
- JWT library directly unless wrapped behind a contract
Elysia rules
Keep routes thin
An Elysia route should do only this:
- validate request input
- map it into the application input
- call a use case
- map the result into HTTP response format
- translate known errors into status codes
Routes must not:
- contain business rules
- access Redis directly
- access SQL directly
- contain cache invalidation logic
- perform complex transformation logic
- implement retries or external integration flows
Use plugins for cross-cutting concerns only
Use Elysia plugins for concerns such as:
- auth context
- request id
- logging
- rate limiting
- tracing
- metrics
- shared error handling
Do not hide business use cases inside framework plugins.
Validate at the edge
Validate request body, params, query, and headers in the presentation layer.
Validation at the HTTP edge does not replace domain invariants.
Redis rules
Redis must always be used through explicit abstractions.
Never scatter raw Redis access across modules.
Valid Redis use cases
Redis may be used for:
- caching
- sessions
- rate limiting
- distributed locks
- pub/sub
- streams
- idempotency keys
- queue support
- temporary tokens
Redis abstraction rule
Create narrow contracts such as:
CacheServiceSessionStoreRateLimitStoreDistributedLockServiceIdempotencyStore
Implement them in infrastructure.
Example:
shared/
application/
contracts/
cache.service.ts
session.store.ts
rate-limit.store.ts
infrastructure/
redis/
redis-cache.service.ts
redis-session.store.ts
redis-rate-limit.store.tsRedis key rule
All keys must follow a naming convention.
Pattern:
app:env:module:resource:identifierExamples:
minuto90:prod:auth:session:user_123
minuto90:prod:notifications:delivery:job_456
minuto90:prod:sports:score:match_789Cache key construction must be centralized in dedicated files.
TTL rule
Every Redis write must have an intentional expiration policy or a documented reason not to expire.
Before storing anything, define:
- why it is stored
- how long it lives
- what invalidates it
- whether stale data is acceptable
Cache strategy rule
Prefer one consistent caching strategy per use case.
For cache-aside flows:
- read cache first
- fall back to source of truth
- repopulate cache with TTL
- invalidate or refresh on writes when needed
Do not mix random caching approaches in the same domain without a reason.
Locking rule
If using distributed locks:
- always set expiration
- use ownership tokens
- only release if token ownership matches
- avoid indefinite locks
Rate limiting rule
Rate limiting must be centralized and reusable, not rewritten per route.
Persistence rules
Repository role
A repository implementation should:
- fetch and persist data
- translate raw database records into domain concepts
- hide persistence details from the application layer
A repository should not:
- make business decisions
- send notifications
- trigger unrelated side effects
- manage transport logic
Query extraction
If a query becomes complex, extract it into a dedicated file or query object.
Do not let repository files become giant unreadable persistence scripts.
Transaction boundary
Transaction orchestration belongs to the application layer through explicit abstractions.
Do not let routes or random repositories define business transaction flows implicitly.
Shared vs module-specific code
Put code in shared/ only when it is truly cross-cutting and reusable across modules.
Examples of valid shared code:
- base error abstractions
- shared cache contracts
- shared Redis implementations
- logger adapters
- config parsing
- HTTP middleware/plugins
- queue adapters
Do not move code to shared/ too early. If the behavior belongs clearly to one module, keep it inside that module.
Workers and background jobs
Use workers/ for independent async processing flows.
A worker should:
- call application-layer use cases or orchestrators
- remain decoupled from HTTP concerns
- have clear input contracts
- produce structured logs
- handle retries intentionally
A worker must not contain hidden business logic that duplicates the main application flow.
Error handling
Use typed errors
Expected failures must use explicit error types.
Examples:
UserNotFoundErrorInvalidCredentialsErrorNotificationDeliveryErrorMatchNotFoundErrorRateLimitExceededErrorInvalidSessionError
Do not throw generic Error for expected business outcomes.
Translate errors at the edge
Map application and domain errors to HTTP responses in presentation.
Examples:
- not found ->
404 - invalid input ->
400 - invalid credentials ->
401 - forbidden ->
403 - conflict ->
409 - rate limit exceeded ->
429
Never leak infrastructure exceptions directly to clients.
Configuration rules
Centralize env parsing
Environment variables must be loaded and validated in a dedicated config layer.
Do not access process.env throughout the codebase.
Recommended:
config/
env.ts
app.config.tsConfiguration must be typed
Expose configuration through stable typed objects, not raw strings.
Logging and observability
Structured logging only
Logs must be structured and contextual.
Include when relevant:
- request id
- route
- module
- user id if available
- latency
- job id
- retry count
- error code
Operational visibility
A robust backend should support, as appropriate:
- health checks
- readiness checks
- metrics
- tracing hooks
- external dependency visibility
- queue monitoring
- retry visibility
Testing rules
Test at the correct level.
- domain tests for business rules
- application tests for use cases
- integration tests for repositories and infrastructure adapters
- route/integration tests for HTTP contracts
- worker tests for async processing behavior
Mock abstractions, not framework internals, unless the goal is integration testing.
Security rules
Apply secure defaults everywhere.
Include as needed:
- input validation
- authorization checks
- authentication boundaries
- rate limiting
- safe session/token handling
- secret isolation
- least privilege infrastructure access
- safe error exposure
- audit-aware logging for sensitive operations
Never log:
- passwords
- raw access tokens
- refresh tokens
- secrets
- API keys
- private keys
- sensitive personal data unless explicitly required and controlled
Review rules for an existing codebase
When reviewing or refactoring an existing backend, apply these checks:
- Is the root organization based on modules or is it mixed with technical folders?
- Do all modules follow the same convention?
- Are subdomains, providers, and adapters clearly differentiated?
- Is there any
lib,utils,helpers, or giantservice.tsacting as a dumping ground? - Are DTOs placed consistently?
- Is Redis abstracted behind contracts?
- Are routes thin?
- Is any business logic leaking into infrastructure or presentation?
- Are names explicit?
- Does each file have one reason to change?
Expected refactor direction
When the codebase shows a hybrid structure, refactor toward these outcomes:
- unify naming and folder conventions across all modules
- remove generic catch-all folders
- move technical providers under infrastructure
- move DTOs under application or presentation
- add missing
domainfolders where business rules exist - isolate Redis behind shared contracts and implementations
- split large files into use-case-specific files
- make routes thinner
- reduce architectural inconsistencies between modules
Example: notifications module
Do not structure a module like this if channel types are mixed with architecture layers:
notifications/
application/
infrastructure/
presentation/
whatsapp/Instead, choose one consistent model.
If channels are infrastructure adapters:
notifications/
domain/
application/
infrastructure/
channels/
push/
whatsapp/
presentation/Example: sports module
If football and nba are truly distinct domains:
sports/
football/
domain/
application/
infrastructure/
presentation/
nba/
domain/
application/
infrastructure/
presentation/If they are only data providers or variants of the same domain, keep them inside one sports module and separate them through contracts and adapters.
Non-negotiable rules
- no business logic in routes
- no raw Redis usage in use cases
- no raw SQL or ORM usage in routes
- no
utils.ts,helpers.ts,misc.ts, or similar dumping grounds - no giant service files
- no vague naming
- no DTO folders floating inconsistently
- no framework leakage into domain
- no technical adapters placed as pseudo-modules without clear intent
- no hidden side effects in mappers or repositories
- no Redis writes without an explicit TTL policy or documented reason
Implementation workflow
When building or refactoring backend functionality, follow this order:
- identify the business module involved
- determine whether subareas are real modules or only adapters/providers
- define domain concepts and boundaries
- define use case inputs/outputs
- define required contracts
- implement infrastructure adapters
- implement thin presentation routes
- add validation and error mapping
- add tests at the correct layer
- review naming, file granularity, and architectural consistency
Final principle
A backend is not solid because it works today.
It is solid when new features can be added without collapsing boundaries, duplicating logic, or turning the codebase into a negotiation with chaos.
This skill must always optimize for that standard.