Preview — Jelou Functions is in preview. The API and behavior may change without prior notice.
When to use app() vs define()?
Pattern When to use it define()A single operation with one HTTP endpoint app()Multiple related tools you want to deploy together
If your project has a single handler, use define(). If you need to group multiple operations — for example, sending and reading emails from the same service — use app().
Basic example
import { app , define , z } from "@jelou/functions" ;
export default app ({
config: { cors: { origin: "*" }, timeout: 30_000 } ,
tools: {
sendEmail: define ({
description: "Sends an email" ,
input: z . object ({ to: z . string (), subject: z . string (), body: z . string () }),
config: { timeout: 5_000 },
handler : async ( input ) => ({ sent: true }),
}),
readInbox: define ({
description: "Reads inbox messages" ,
input: z . object ({ limit: z . coerce . number (). default ( 10 ) }),
handler : async ( input ) => ({ messages: [] }),
}),
} ,
}) ;
Local testing
Start the server with jelou dev and test each tool at its route:
curl /send-email
Response 200
curl -X POST http://localhost:3000/send-email \
-H "Content-Type: application/json" \
-d '{"to": "[email protected] ", "subject": "Hello", "body": "Welcome"}'
curl /read-inbox
Response 200
curl http://localhost:3000/read-inbox?limit= 5
If you send an invalid field, you receive a 400 with details:
curl with invalid input
Response 400
curl -X POST http://localhost:3000/send-email \
-H "Content-Type: application/json" \
-d '{"to": ""}'
API
import { app } from "@jelou/functions" ;
const edgeApp = app ({
config: { cors: { origin: "*" }, timeout: 30_000 },
tools: {
/* ... your define() here ... */
},
});
export default edgeApp ;
Type signature:
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 ;
}
Field Type Default Description corsobject{ origin: "*" }Global CORS configuration timeoutnumber30000Timeout in ms for all tools methodsstring[]["GET","POST","PUT","PATCH","DELETE"]Allowed HTTP methods mcpbooleantrueRegister tools in the MCP server
Auto-generated routes
Your tools object keys are automatically converted to kebab-case to generate HTTP routes:
Key Route sendEmail/send-emailreadInbox/read-inboxMyTool/my-tool
To customize a route, use config.path in the individual define():
tools : {
sendEmail : define ({
config: { path: "/emails/send" },
handler : async ( input ) => ({ sent: true }),
}),
}
Config combination
The global config of app() applies to all tools. Each define() can override specific values:
Field Behavior timeoutTool overrides the global methodsTool overrides the global corsTool overrides the global mcpTool overrides the global pathAlways per tool cronAlways per tool
export default app ({
config: { timeout: 30_000 , cors: { origin: "*" } } ,
tools: {
fast: define ({
config: { timeout: 5_000 },
handler : async () => ({ ok: true }),
}),
slow: define ({
handler : async () => ({ ok: true }),
}),
} ,
}) ;
In this example, fast has a 5-second timeout and slow inherits the global 30 seconds. Both share the CORS configuration.
Health endpoint
In app mode, /__health returns information about all tools:
{
"mode" : "app" ,
"tools" : [
{
"key" : "sendEmail" ,
"name" : "Send Email" ,
"description" : "Sends an email" ,
"path" : "/send-email" ,
"cron" : []
},
{
"key" : "readInbox" ,
"name" : "Read Inbox" ,
"description" : "Reads inbox messages" ,
"path" : "/read-inbox" ,
"cron" : []
}
]
}
Type guards
Use isEdgeApp() and isEdgeFunction() to identify the export type at runtime:
import { isEdgeApp , isEdgeFunction } from "@jelou/functions" ;
isEdgeApp ( module ); // true if created with app()
isEdgeFunction ( module ); // true if created with define()
Each tool inside app() can have its own independent cron schedules. Cron requests are sent to the specific route of each tool.
export default app ({
tools: {
dailyCleanup: define ({
description: "Cleans up stale records" ,
input: z . object ({}),
config: { cron: [{ expression: "0 3 * * *" , timezone: "UTC" }] },
handler : async ( _input , ctx ) => {
if ( ! ctx . isCron ) return { skipped: true };
return { cleaned: true };
},
}),
hourlySync: define ({
description: "Syncs external data" ,
input: z . object ({}),
config: { cron: [{ expression: "0 * * * *" }] },
handler : async ( _input , ctx ) => {
if ( ! ctx . isCron ) return { skipped: true };
return { synced: true };
},
}),
} ,
}) ;
The 10 cron schedule limit is aggregated across all tools in an app(). If dailyCleanup has 3 and hourlySync has 4, only 3 remain available.
A single MCP server at /mcp automatically registers all tools from the app(). To exclude a tool from the MCP registry, use mcp: false in its config:
export default app ({
tools: {
publicTool: define ({
description: "Visible to MCP" ,
input: z . object ({}),
handler : async () => ({}),
}),
internalTool: define ({
description: "HTTP only" ,
input: z . object ({}),
config: { mcp: false },
handler : async () => ({}),
}),
} ,
}) ;
In this example, AI agents only discover publicTool via MCP. internalTool is only accessible via direct HTTP at its /internal-tool route.
Exposed routes (app mode)
Route Description /__healthHealth check with tool list /mcpUnified MCP server (unless config.mcp: false globally) /<tool-key>Each tool’s route (kebab-case or custom config.path)
Common issues
Next steps