Skip to main content
Preview — Jelou Functions está en fase de vista previa. La API y el comportamiento pueden cambiar sin previo aviso.

¿Cuándo usar app() vs define()?

PatrónCuándo usarlo
define()Una sola operación con un endpoint HTTP
app()Varias herramientas relacionadas que quieres desplegar juntas
Si tu proyecto tiene un solo handler, usa define(). Si necesitas agrupar múltiples operaciones — por ejemplo, enviar y leer emails desde el mismo servicio — usa app().

Ejemplo básico

index.ts
import { app, define, z } from "@jelou/functions";

export default app({
  config: { cors: { origin: "*" }, timeout: 30_000 },
  tools: {
    enviarEmail: define({
      description: "Envía un correo electrónico",
      input: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
      config: { timeout: 5_000 },
      handler: async (input) => ({ sent: true }),
    }),
    leerBandeja: define({
      description: "Lee los mensajes de la bandeja de entrada",
      input: z.object({ limit: z.coerce.number().default(10) }),
      handler: async (input) => ({ messages: [] }),
    }),
  },
});

Prueba local

Inicia el servidor con jelou dev y prueba cada tool en su ruta:
curl -X POST http://localhost:3000/enviar-email \
  -H "Content-Type: application/json" \
  -d '{"to": "[email protected]", "subject": "Hola", "body": "Bienvenida"}'
curl http://localhost:3000/leer-bandeja?limit=5
Si envías un campo inválido, recibes un 400 con detalles:
curl -X POST http://localhost:3000/enviar-email \
  -H "Content-Type: application/json" \
  -d '{"to": ""}'

API

import { app } from "@jelou/functions";

const edgeApp = app({
  config: { cors: { origin: "*" }, timeout: 30_000 },
  tools: {
    /* ... tus define() aquí ... */
  },
});

export default edgeApp;
Firma de tipo:
function app(options: {
  config?: AppConfig;
  tools: Record<string, AnyEdgeFunction>;
}): EdgeApp;

AppConfig

interface AppConfig {
  cors?: {
    origin?: string | string[];
    methods?: string[];
    headers?: string[];
    credentials?: boolean;
    maxAge?: number;
  };
  timeout?: number;
  methods?: string[];
  mcp?: boolean;
}
CampoTipoPredeterminadoDescripción
corsobject{ origin: "*" }Configuración CORS global
timeoutnumber30000Timeout en ms para todos los tools
methodsstring[]["GET","POST","PUT","PATCH","DELETE"]Métodos HTTP permitidos
mcpbooleantrueRegistrar tools en el servidor MCP

Rutas auto-generadas

Tus keys del objeto tools se convierten automáticamente a kebab-case para generar las rutas HTTP:
KeyRuta
enviarEmail/enviar-email
leerBandeja/leer-bandeja
MiTool/mi-tool
Para personalizar una ruta, usa config.path en el define() individual:
tools: {
  enviarEmail: define({
    config: { path: "/emails/enviar" },
    handler: async (input) => ({ sent: true }),
  }),
}

Combinación de config

La configuración global de app() se aplica a todos los tools. Cada define() puede sobreescribir valores específicos:
CampoComportamiento
timeoutEl tool sobreescribe el global
methodsEl tool sobreescribe el global
corsEl tool sobreescribe el global
mcpEl tool sobreescribe el global
pathSiempre por tool
cronSiempre por tool
export default app({
  config: { timeout: 30_000, cors: { origin: "*" } },
  tools: {
    rapido: define({
      config: { timeout: 5_000 },
      handler: async () => ({ ok: true }),
    }),
    lento: define({
      handler: async () => ({ ok: true }),
    }),
  },
});
En este ejemplo, rapido tiene un timeout de 5 segundos y lento hereda los 30 segundos globales. Ambos comparten la configuración CORS.

Endpoint de salud

En modo app, /__health retorna información de todos los tools:
{
  "mode": "app",
  "tools": [
    {
      "key": "enviarEmail",
      "name": "Enviar Email",
      "description": "Envía un correo electrónico",
      "path": "/enviar-email",
      "cron": []
    },
    {
      "key": "leerBandeja",
      "name": "Leer Bandeja",
      "description": "Lee los mensajes de la bandeja de entrada",
      "path": "/leer-bandeja",
      "cron": []
    }
  ]
}

Guardas de tipo

Usa isEdgeApp() e isEdgeFunction() para identificar el tipo de export en runtime:
import { isEdgeApp, isEdgeFunction } from "@jelou/functions";

isEdgeApp(module);      // true si fue creado con app()
isEdgeFunction(module);  // true si fue creado con define()

Multi-tool cron

Cada tool dentro de app() puede tener sus propios schedules cron independientes. Las peticiones cron se envían a la ruta específica de cada tool.
export default app({
  tools: {
    limpiezaDiaria: define({
      description: "Limpia registros obsoletos",
      input: z.object({}),
      config: { cron: [{ expression: "0 3 * * *", timezone: "UTC" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { cleaned: true };
      },
    }),
    sincronizacionHoraria: define({
      description: "Sincroniza datos externos",
      input: z.object({}),
      config: { cron: [{ expression: "0 * * * *" }] },
      handler: async (_input, ctx) => {
        if (!ctx.isCron) return { skipped: true };
        return { synced: true };
      },
    }),
  },
});
El límite de 10 schedules cron es agregado entre todos los tools de un app(). Si limpiezaDiaria tiene 3 y sincronizacionHoraria tiene 4, quedan 3 disponibles.

Multi-tool MCP

Un solo servidor MCP en /mcp registra automáticamente todos los tools del app(). Para excluir un tool del registro MCP, usa mcp: false en su config:
export default app({
  tools: {
    toolPublico: define({
      description: "Visible para MCP",
      input: z.object({}),
      handler: async () => ({}),
    }),
    toolInterno: define({
      description: "Solo HTTP",
      input: z.object({}),
      config: { mcp: false },
      handler: async () => ({}),
    }),
  },
});
En este ejemplo, los agentes IA solo descubren toolPublico vía MCP. toolInterno solo es accesible por HTTP directo en su ruta /tool-interno.

Rutas expuestas (modo app)

RutaDescripción
/__healthHealth check con lista de tools
/mcpServidor MCP unificado (a menos que config.mcp: false global)
/<tool-key>Ruta de cada tool (kebab-case o config.path personalizado)

Problemas comunes

Las keys de tools se convierten a kebab-case. Si tu key es enviarEmail, la ruta es /enviar-email, no /enviarEmail.Verifica las rutas reales con el health check:
curl http://localhost:3000/__health

Siguientes pasos