npm.io
1.5.0 • Published 6d agoCLI

arckode-framework

Licence
MIT
Version
1.5.0
Deps
1
Size
316 kB
Vulns
0
Weekly
69

Arckode Framework

Framework TypeScript/Bun para construir APIs modulares. Diseñado desde el principio para trabajar con IA: arquitectura predecible, límites claros, cero magia.

arckode new mi-api
cd mi-api && bun install
arckode make:auth
arckode make:module Productos
bun run dev

Por qué Arckode

La mayoría de los frameworks están optimizados para que los humanos escriban código. Arckode está optimizado para que la IA genere código correcto sin supervisión constante.

La IA lee el composition-root.ts y sabe todo lo que necesita:

  • Qué módulos existen y qué hacen
  • Cómo se conectan entre sí
  • Qué datos maneja cada uno
  • Qué reglas tienen que cumplir

No hay decoradores, no hay magia de inyección, no hay convenciones implícitas. Todo está explícito en un solo lugar.


Instalación

Requisito: Bun >= 1.0

# 1. Instalar el CLI globalmente — una sola vez por máquina
bun install -g arckode-framework

# 2. Crear tu proyecto
arckode new mi-api

# 3. Entrar al proyecto e instalar dependencias
cd mi-api && bun install

# 4. Configurar entorno
echo 'JWT_SECRET=cambia-esto-en-produccion' > .env

# 5. Iniciar
bun run src/composition-root.ts

El CLI instala arckode-framework como dependencia de tu proyecto automáticamente. No necesitás referenciar rutas del framework — todo se importa como from 'arckode-framework'.


Inicio rápido

arckode new mi-api
cd mi-api && bun install
arckode make:auth           # módulo completo de auth con JWT
arckode make:module Clientes
arckode analyze             # 0 violaciones garantizado
bun run dev                 # hot reload

Conceptos core

Módulo

La unidad básica del sistema. Cada módulo es dueño de sus datos y su lógica. Nunca importa de otro módulo directamente.

modules/productos/
  index.ts          ← puerta pública (solo exports)
  types.ts          ← DTOs y ModelDefinition
  sockets.ts        ← hooks para conectores (opcional)
  actions/
    service.ts      ← lógica de negocio
    controller.ts   ← capa HTTP (sin lógica)
  validators/
    schema.ts       ← validación de entrada
  tests/
    service.test.ts
// modules/productos/index.ts
export function ProductosModule() {
  return createModule({
    name: 'productos',
    version: '1.0.0',
    description: 'Gestión del catálogo de productos',
    contract: {
      actions: ['listar', 'crear', 'actualizar', 'eliminar'],
      events: ['onProductoCreado'],
      tables: ['productos'],
    },
    create({ logger, orm, cache, router }) {
      const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
      const service = new ProductosService(repo, logger.child('productos'), cache)
      const controller = new ProductosController(service, logger.child('productos'))

      router.get('/productos', (req) => controller.index(req))
      router.post('/productos', (req) => controller.store(req))
      router.put('/productos/:id', (req) => controller.update(req))
      router.delete('/productos/:id', (req) => controller.destroy(req))

      return service  // resolveModule('productos') devuelve esto
    },
  })
}
RepositoryAdapter<T>

Los servicios nunca dependen del ORM directamente. Dependen de la interfaz genérica — el ORM concreto se elige en composition-root.ts.

// ❌ PROHIBIDO — acoplado al ORM, imposible cambiar a MongoDB/Prisma
class ProductosService {
  constructor(private orm: ORM) {}
  listar() { return this.orm.findMany('Producto') }
}

// ✅ CORRECTO — desacoplado, testeable, intercambiable
class ProductosService {
  constructor(private repo: RepositoryAdapter<ProductoDTO>) {}
  listar() { return this.repo.findMany() }
}
// composition-root.ts — se elige la implementación una sola vez
const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')  // SQLite/Postgres
// O mañana:
const repo = new MongoProductoRepo(collection)                 // MongoDB
// El service nunca cambia.
Connector

El único puente entre módulos. Solo delegación — nunca lógica de negocio.

// connectors/pedido-stock.ts
export function conectarPedidoConStock(ctx: ConnectorContext): void {
  const productos = ctx.resolveModule<ProductosService>('productos')

  ctx.resolveModule('pedidos', {
    onPedidoCreado: async (pedido) => {
      await productos.descontarStock(pedido.productoId, pedido.cantidad)
    },
  })
}
Composition Root

El único archivo que conoce todo el sistema. La IA lo lee y entiende la arquitectura completa.

// src/composition-root.ts
import { ConfigStore, ORM, Router, NodeServer, MemoryCache, System, Auth, loadEnv } from 'arckode-framework'
import { SqliteAdapter } from 'arckode-framework/adapters/sqlite'
import { jwtTokenAdapter } from 'arckode-framework/adapters/jwt'
import { ProductoModel } from './modules/productos/types'
import { ProductosModule } from './modules/productos'
import { PedidosModule } from './modules/pedidos'
import { conectarPedidoConStock } from './connectors/pedido-stock'

const env = await loadEnv()  // carga .env + .env.{NODE_ENV}

const config = new ConfigStore()
config.define({
  PORT:       { type: 'number', default: 3000 },
  DB_PATH:    { type: 'string', default: './data/db.sqlite' },
  JWT_SECRET: { type: 'string', required: true },
}).load(env)

const db = new SqliteAdapter({ path: config.get('DB_PATH') })
await db.connect()
const orm = new ORM(db)

orm.define('Producto', ProductoModel)  // cada módulo es dueño de su ModelDefinition
await orm.migrate()

const system = new System({ config, orm, router, http, cache, auth, ... })
system.addModule(ProductosModule())
system.addModule(PedidosModule())
system.addConnector('pedido-stock', conectarPedidoConStock)
await system.start()

CLI — Referencia completa

Proyectos
Comando Descripción
arckode new <nombre> Crear proyecto backend completo
arckode new:frontend [nombre] Crear frontend Vue 3 + Vite + TypeScript
Generadores de backend
Comando Descripción
arckode make:auth Módulo de autenticación completo (login, register, JWT, perfil)
arckode make:module <Nombre> Módulo con service, controller, types, validators, tests
arckode make:connector <nombre> <mod1> <mod2> Conector entre módulos
arckode make:seed <Nombre> Seed de datos
arckode make:migration <nombre> Migración SQL con up() y down()
arckode make:helper <nombre> Helper puro (sin efectos secundarios)
arckode make:adapter <Adapter> <Interfaz> Adapter de librería externa
Generadores de frontend
Comando Descripción
arckode make:page <Nombre> Módulo frontend (API client + composable + página + router)
arckode generate:api [frontend-path] Genera API clients desde módulos del backend
Base de datos
Comando Descripción
arckode db:migrate Ejecutar migraciones pendientes (src/migrations/)
arckode db:migrate down Revertir la última migración
arckode db:seed Listar seeds disponibles
Análisis y diagnóstico
Comando Descripción
arckode analyze Detecta 15+ tipos de violaciones de arquitectura
arckode routes Lista todas las rutas registradas (análisis estático)

Adapters

Base de datos
// SQLite — desarrollo y apps de baja escala
import { SqliteAdapter } from 'arckode-framework/adapters/sqlite'
const db = new SqliteAdapter({ path: './data/app.sqlite' })

// PostgreSQL — producción
import { PostgresAdapter } from 'arckode-framework/adapters/postgres'
const db = new PostgresAdapter({ connectionString: process.env.DATABASE_URL, poolMax: 10 })
Cache
// En memoria — desarrollo (se pierde al reiniciar)
const cache = new MemoryCache()

// Redis — producción
import { RedisCacheAdapter } from 'arckode-framework/adapters/redis-cache'
const cache = new RedisCacheAdapter({ url: process.env.REDIS_URL })
await cache.connect()

Módulos opcionales

Queue
import { QueueService, MemoryQueueAdapter } from 'arckode-framework/queue'

const queue = new QueueService(new MemoryQueueAdapter())

queue.register('enviar-email', async (job) => {
  await mail.send(job.data as EmailData)
})

await queue.dispatch('enviar-email', { to: 'user@example.com', subject: 'Bienvenido' })
await queue.dispatch('enviar-email', { ... }, { delay: 5000, maxAttempts: 3 })
Events (pub-sub)
import { EventBus } from 'arckode-framework/events'

const events = new EventBus()
events.on('pedido.creado', async (pedido) => { ... })
events.emit('pedido.creado', pedido)
WebSockets
import { WsServer } from 'arckode-framework/ws'

const ws = new WsServer()
ws.on('connection', (client) => { client.send({ type: 'welcome' }) })
ws.broadcast({ type: 'stock-bajo', productoId: id })
Mail
import { MailService } from 'arckode-framework/mail'
import { SmtpAdapter } from 'arckode-framework/mail/smtp'

const mail = new MailService(new SmtpAdapter({ host: 'smtp.gmail.com', port: 587, ... }))
await mail.send({ to: 'user@example.com', subject: 'Bienvenido', html: '<p>Hola</p>' })
Storage
import { StorageService } from 'arckode-framework/modules/storage'
import { LocalStorageAdapter } from 'arckode-framework/modules/storage/local-adapter'

// En composition-root.ts
const storage = new StorageService(
  new LocalStorageAdapter('./uploads', '/uploads')
)

// Subir un archivo
const stored = await storage.upload(file, 'avatars')
// stored.url  → '/uploads/avatars/1234567890-abc.jpg'
// stored.path → 'avatars/1234567890-abc.jpg'
File Uploads (multipart/form-data)

El servidor detecta Content-Type: multipart/form-data automáticamente y parsea el body sin dependencias externas. Los archivos quedan en req.files, los campos de texto en req.body.

import type { UploadedFile } from 'arckode-framework'
import { ValidationError } from 'arckode-framework'

router.post('/avatar', [auth.authenticate()], async (req) => {
  const file = req.files?.['avatar']
  if (!file) throw new ValidationError('Se requiere un archivo')

  const stored = await storageService.upload(file, 'avatars')
  return { status: 200, body: { url: stored.url } }
})

Desde el cliente:

const form = new FormData()
form.append('avatar', blob, 'photo.jpg')
await fetch('/avatar', { method: 'POST', body: form })

Estructura de UploadedFile:

interface UploadedFile {
  fieldName: string     // nombre del campo en el form
  originalName: string  // nombre del archivo enviado
  buffer: Buffer        // contenido binario
  mimeType: string      // 'image/jpeg', 'application/pdf', etc.
  size: number          // bytes
}

Combiná con bodyLimit() para limitar el tamaño máximo:

router.post('/avatar', [bodyLimit(5 * 1024 * 1024)], handler)  // 5MB máx

Middlewares

import { cors, rateLimit, requestLogger, timeout, compression, bodyLimit } from 'arckode-framework/middlewares'

// Globales
router.use(cors({ origins: ['https://miapp.com'] }))
router.use(rateLimit({ windowMs: 60_000, max: 100 }))
router.use(requestLogger(logger))
router.use(compression())

// Por ruta
router.get('/admin', handler, [auth.authenticate('admin'), timeout(3000)])
router.post('/upload', handler, [bodyLimit(10 * 1024 * 1024)])  // 10MB

Auth

const auth = new Auth(jwtTokenAdapter, process.env.JWT_SECRET, logger)

// Crear token
const token = await auth.createToken({ id: user.id, role: 'admin' })

// Hashear password (scrypt — sin dependencias externas)
const hash = await auth.hashPassword(password)
const ok   = await auth.comparePassword(password, hash)

// Proteger rutas
router.get('/perfil', handler, [auth.authenticate()])          // cualquier usuario autenticado
router.get('/admin',  handler, [auth.authenticate('admin')])   // solo admins
router.delete('/users/:id', handler, [auth.authenticate('admin', 'superadmin')])

Variables de entorno

loadEnv() carga .env base y .env.{NODE_ENV} con override por stage. process.env siempre tiene prioridad máxima.

# .env
PORT=3000
DB_PATH=./data/dev.sqlite
LOG_LEVEL=debug

# .env.production
DB_PATH=./data/prod.sqlite
LOG_LEVEL=warn
NODE_ENV=production bun run src/composition-root.ts
# Lee .env → sobrescribe con .env.production → process.env tiene prioridad

Testing

import { createTestClient, createRecordingOrm } from 'arckode-framework/testing'
import { OrmRepository } from 'arckode-framework'

// ORM que registra llamadas en memoria — sin base de datos real
const orm = createRecordingOrm()
const repo = new OrmRepository<ProductoDTO>(orm, 'Producto')
const service = new ProductosService(repo, logger, cache)

// Cliente HTTP que hace requests al Router sin levantar un server real
const client = createTestClient(router)

test('listar productos vacío', async () => {
  const res = await client.get('/productos')
  expect(res.status).toBe(200)
  expect(res.body).toEqual([])
})

test('crear producto', async () => {
  const res = await client.post('/productos', {
    body: { nombre: 'Laptop', precio: 1500, stock: 10 },
  })
  expect(res.status).toBe(201)
  expect(res.body.nombre).toBe('Laptop')
})

Análisis de arquitectura

arckode analyze
══════════════════════════════════════════════
  Arckode — Análisis de Arquitectura
══════════════════════════════════════════════

VIOLACIONES ENCONTRADAS: 2

[Acoplamiento]
  ❌ modules/pedidos/actions/service.ts:12
     Importa directamente de otro módulo (CLAUDE #1)
     → Usar un conector en /connectors/

[Portabilidad]
  ❌ modules/clientes/actions/service.ts:8
     El service inyecta ORM directamente (CLAUDE #18)
     → Usar RepositoryAdapter<ClienteDTO>
Categoría Violations detectadas
Estructura MISSING_INDEX, MISSING_TYPES, MISSING_SERVICE, MISSING_CONTROLLER, MISSING_TESTS
Acoplamiento DIRECT_MODULE_IMPORT
Diseño BUSINESS_LOGIC_IN_CONTROLLER, CONTROLLER_MISSING_VALIDATION, EMPTY_MODULE_DESCRIPTION
Calidad TESTS_WITHOUT_CASES, GOD_SERVICE
Seguridad IDOR_RISK, HARDCODED_SECRET, INSECURE_PASSWORD
Performance N_PLUS_ONE_RISK
Portabilidad SERVICE_DEPENDS_ON_ORM

Ejemplos incluidos

Ejemplo Descripción
examples/ecommerce/ Productos, pedidos, conector de stock, auth
examples/completo/ Auth, productos, pedidos, mail, storage, queue, WebSockets

Documentación adicional

Archivo Contenido
CLAUDE.md 18 reglas inmutables — el contrato que la IA sigue
kernel/framework.ts Fuente completa del kernel (la IA lo lee entero)
kernel/testing.ts Utilidades de testing
kernel/middlewares.ts Middlewares disponibles

Cómo funciona la distribución

Para usuarios del framework
# Instalar CLI una vez
bun install -g arckode-framework

# Crear proyecto — el CLI genera todo: composition-root.ts, CLAUDE.md, .env, package.json
arckode new mi-tienda --db=postgres

# CLAUDE.md se genera automáticamente en tu proyecto con las 18 reglas del framework.
# La IA lo lee primero antes de escribir cualquier línea de código.

# Actualizar el framework en tu proyecto
bun update arckode-framework
Para la IA (flujo de trabajo)

Cuando la IA trabaja en un proyecto con Arckode, hace esto en orden:

1. Lee CLAUDE.md         → entiende las 18 reglas que NO puede violar
2. Lee composition-root.ts → entiende todos los módulos, conectores y dependencias
3. Corre arckode analyze  → verifica que el estado actual tiene 0 violaciones
4. Genera código          → siguiendo la estructura exacta del framework
5. Corre arckode analyze  → verifica que sus cambios no introdujeron violaciones
6. Corre bun test         → verifica que nada se rompió
Para el autor del framework (actualizaciones)
# 1. Hacer los cambios
# 2. Correr tests y typecheck
bun test && bun run typecheck

# 3. Subir la versión en package.json (semver)
#    1.0.0 → 1.0.1  patch: bugfix
#    1.0.0 → 1.1.0  minor: feature nueva, retrocompatible
#    1.0.0 → 2.0.0  major: breaking change

# 4. Publicar
npm publish

# Los usuarios actualizan corriendo en su proyecto:
bun update arckode-framework

Adapters opcionales

Arckode solo instala lo que usás. Cada adapter tiene su dependencia peer:

Adapter Instalar
SQLite bun add better-sqlite3
PostgreSQL bun add pg
MySQL bun add mysql2
Redis (cache) bun add redis
Mail (SMTP) bun add nodemailer

El framework core (kernel/framework.ts) no depende de ninguna librería externa — solo de Node.js/Bun nativo.


Licencia

MIT

Keywords