SaaS Starter

Estructura del proyecto

Layout del monorepo, capas DDD y dónde vive cada cosa.

Nivel superior

apps/
  client/                Next.js — landing, dashboard, auth, /docs
  server/                Express — API, auth, billing, jobs
packages/
  contracts/             Schemas Zod compartidos + tipos inferidos
  shared/                Catálogo de permissions, Result, errors, branded types
  tsconfig/              tsconfig bases
  eslint-config/         flat configs que aplican el layering DDD
tests/                   Playwright e2e

El repo es un workspace de Bun. Todo import cross-package interno pasa por @app/contracts, @app/shared, etc. — nunca relativo entre apps.

apps/server

src/
  index.ts               Entry point. otel-init DEBE ser el primer import.
  otel-init.ts           Arranca OTel antes que cualquier otra cosa.
  bootstrap/
    env.ts               Schema Zod para env. Fuente de verdad.
    container.ts         Registry de DI con Awilix.
    app.ts               Construye la app de Express y monta todos los routers bajo /api.
    worker.ts            Bootstrap del worker BullMQ.
  infrastructure/
    db/                  Cliente Prisma + helpers.
    http/                Helper api-route, error handler, rate-limit, registry de OpenAPI, requirePermission.
    events/              Bus de eventos en memoria.
    jobs/                Factories de queue + worker BullMQ, Bull Board.
    cache/               Adapter de Redis (no-op cuando REDIS_URL no está).
    logger/              Setup de pino.
    observability/       Sentry, exporters de OTel, helper captureError.
    i18n/                Runtime de traducción server-side.
  modules/<context>/     Bounded contexts (ver abajo).
  shared/
    aggregate-root.ts    Base AggregateRoot + tipo DomainEvent.
    usecase.ts           Contrato UseCase<Input, Output>.
    value-object.ts      Base de value object.

Capas DDD dentro de un módulo

Todo módulo bajo apps/server/src/modules/<context>/ sigue el mismo layout:

domain/                  Entidades, value objects, repository interfaces.
                         Sin I/O. Sin imports de framework. Sin Prisma. Sin Express.
application/             Use cases, ports (output adapters), DTOs.
infrastructure/          Repos de Prisma, adapters de terceros, mappers.
interfaces/http/         Controladores Express, registración de rutas, schemas Zod.

La regla de dependencias es sólo hacia adentro: interfaces/ depende de application/, que depende de domain/. Infrastructure implementa los ports declarados en application/ (o las repository interfaces declaradas en domain/).

ESLint lo aplica. domain/ no puede importar express, @prisma/client, ni nada desde application/ / infrastructure/ / interfaces/. Probá — el import es flagged antes del commit.

Los bounded contexts incluidos hoy: iam, tenancy, billing, notifications, storage, audit, feature-flags. Ver Bounded contexts.

apps/client

app/
  page.tsx, es/, pt/     Landing pages (i18n).
  _landing/              Componentes de landing + diccionario.
  (auth)/                Sign-in, sign-up, forgot-password.
  (dashboard)/           Surface autenticada de la app.
  docs/[[...slug]]/      Fumadocs.
  api/search/            Endpoint de búsqueda Orama.
  globals.css            Tokens (OKLCH) + base + utilities.
  layout.tsx             Carga Geist Sans + Geist Mono una sola vez.
components/
  brand/                 Primitives del design system UseDeploy. Leer /docs/es/frontend.
  ui/                    Primitives derivados de shadcn.
content/docs/            El MDX que estás leyendo.
lib/
  api/                   Cliente openapi-fetch + tipos generados.
  validations/           Refinements client-only construidos sobre @app/contracts.
  source.ts              Content source de Fumadocs.

El client habla con el server vía openapi-fetch y los paths tipados en lib/api/openapi-types.ts. Nunca redefinas un campo conocido del server en un schema Zod del client — extendé el contract, no lo bifurques.

packages/contracts

Schemas Zod + tipos inferidos que cruzan el límite client/server. Forms y mutations importan desde acá:

src/
  auth.ts                Login, register, password reset.
  user.ts                Perfil de user, update, list.
  common.ts              Primitives compartidos (paginación, IDs).
  index.ts               Barrel.

Si un endpoint del server cambia su body o response, el contract cambia acá, el OpenAPI se regenera (bun run generate:api), y el client compile-checkea la nueva forma.

packages/shared

src/
  permissions/           Catálogo resource:action + helper hasPermission.
  result/                Result<T, E> + constructores ok/err.
  errors/                DomainError, UnauthorizedError, ForbiddenError, etc.
  types/                 Tipos branded para IDs.
  index.ts               Barrel.

Importado como @app/shared desde ambas apps.

En esta página