Project Structure
VentureKit uses convention-based directories to discover and deploy your code. Here’s the recommended project structure:
my-app/├── vk.config.ts # VentureKit configuration├── src/│ ├── routes/ # API route handlers (→ API Gateway + Lambda)│ │ ├── health/│ │ │ └── get.ts│ │ ├── users/│ │ │ ├── get.ts # GET /users│ │ │ ├── post.ts # POST /users│ │ │ └── [id]/│ │ │ ├── get.ts # GET /users/{id}│ │ │ └── put.ts # PUT /users/{id}│ │ └── orders/│ │ └── post.ts # POST /orders│ ││ ├── functions/ # Standalone Lambda functions (→ Lambda only, no API Gateway)│ │ ├── process-orders.ts # → {project}-{stage}-fn-process-orders│ │ ├── send-email.ts # → {project}-{stage}-fn-send-email│ │ └── order/│ │ └── create-order.ts # → {project}-{stage}-fn-order-create-order│ ││ ├── queues/ # SQS queue consumers (→ SQS + Lambda)│ │ └── order-events.ts # → {project}-{stage}-queue-order-events│ ││ ├── crons/ # Scheduled tasks (→ EventBridge rule + Lambda)│ │ └── daily-report.ts # → {project}-{stage}-cron-daily-report│ ││ └── lib/ # Shared logic (NOT deployed as separate functions)│ ├── db.ts # Database client / queries│ ├── email.ts # Email service│ ├── validation.ts # Shared validation schemas│ └── pricing.ts # Business logic shared across routes & functions│├── package.json└── tsconfig.jsonRoutes (src/routes/)
Section titled “Routes (src/routes/)”Route handlers are automatically mapped to API Gateway endpoints based on their file path and name. Each file becomes a separate Lambda function fronted by API Gateway.
See File-Based Routing for details.
Functions (src/functions/)
Section titled “Functions (src/functions/)”Standalone Lambda functions that are not exposed via API Gateway. Use these for:
- Saga workers — steps in a distributed transaction
- Queue consumers — process SQS messages
- Background processors — image resizing, report generation
- Scheduled tasks — cron jobs, cleanup routines
- Notification dispatchers — email, SMS, push
Naming Convention
Section titled “Naming Convention”The function name is derived from its file path under src/functions/:
| File | Function Name | AWS Lambda Name |
|---|---|---|
process-orders.ts | process-orders | {project}-{stage}-fn-process-orders |
order/create-order.ts | order-create-order | {project}-{stage}-fn-order-create-order |
send-email.ts | send-email | {project}-{stage}-fn-send-email |
Invoking Functions
Section titled “Invoking Functions”import { invoke } from '@venturekit/runtime';
// Call a standalone function by nameconst result = await invoke({ function: 'process-orders' }, { payload: { orderId: 'abc-123', items: [...] },});
// Fire-and-forgetawait invoke({ function: 'send-email' }, { payload: { to: 'user@example.com', template: 'welcome' }, mode: 'async',});Local Development
Section titled “Local Development”During vk dev, standalone functions are discovered automatically and can be invoked via HTTP:
# Invoke a function locallycurl -X POST http://localhost:3000/_dev/invoke/process-orders \ -H "Content-Type: application/json" \ -d '{"orderId": "abc-123"}'
# List all discovered functions and routescurl http://localhost:3000/_dev/functionsConfiguration
Section titled “Configuration”By default, VentureKit looks for functions in src/functions/. Override this in vk.config.ts:
export default defineVenture({ base, security, envs: { dev, prod }, routesDir: 'src/routes', functionsDir: 'src/functions', // default});You can also configure individual functions with custom memory/timeout via infrastructure intents:
infrastructure: { functions: [ { id: 'process-orders', memorySize: 512, timeout: 60 }, { id: 'send-email', description: 'Sends transactional emails' }, ],}Queues (src/queues/)
Section titled “Queues (src/queues/)”Queue consumer Lambda functions that process messages from SQS queues. Each file becomes a Lambda wired to an SQS queue via an event source mapping.
Naming Convention
Section titled “Naming Convention”| File | Queue Name | AWS Lambda Name |
|---|---|---|
order-events.ts | {project}-{stage}-order-events | {project}-{stage}-queue-order-events |
notifications.ts | {project}-{stage}-notifications | {project}-{stage}-queue-notifications |
Writing a Queue Consumer
Section titled “Writing a Queue Consumer”import { taskHandler } from '@venturekit/runtime';
export const main = taskHandler<{ Records: Array<{ body: string }> }>( async (event, ctx, logger) => { for (const record of event.Records) { const message = JSON.parse(record.body); logger.info('Processing', { message }); } return { processed: event.Records.length }; }, { retries: 1 },);Configuration
Section titled “Configuration”Configure queue infrastructure and handler settings together via QueueIntent:
infrastructure: { queues: [ { id: 'order-events', // matches src/queues/order-events.ts deadLetterQueue: true, // move failed messages to DLQ batchSize: 5, // messages per invocation timeout: 30, // Lambda timeout (seconds) memorySize: 256, // Lambda memory (MB) }, ],}Retry Architecture
Section titled “Retry Architecture”Queue consumers have two layers of retries:
- Application retries —
taskHandler({ retries: N })retries within a single invocation for transient errors. - Infrastructure retries — when the Lambda throws, SQS re-delivers the message after
visibilityTimeout. AftermaxReceiveCountfailures, the message moves to the dead-letter queue.
Local Development
Section titled “Local Development”curl -X POST http://localhost:3000/_dev/invoke/queue/order-events \ -H "Content-Type: application/json" \ -d '{"orderId": "abc-123", "action": "created"}'The dev server wraps your payload in an SQS-like event structure automatically.
Crons (src/crons/)
Section titled “Crons (src/crons/)”Scheduled task Lambda functions triggered by EventBridge rules. Each file becomes a Lambda with an EventBridge schedule.
Naming Convention
Section titled “Naming Convention”| File | AWS Lambda Name | EventBridge Rule |
|---|---|---|
daily-report.ts | {project}-{stage}-cron-daily-report | {project}-{stage}-cron-daily-report |
cleanup.ts | {project}-{stage}-cron-cleanup | {project}-{stage}-cron-cleanup |
Writing a Cron Handler
Section titled “Writing a Cron Handler”import { taskHandler } from '@venturekit/runtime';
export const main = taskHandler( async (_event, ctx, logger) => { logger.info('Running daily report', { time: ctx.timestamp.toISOString() }); return { report: { ordersProcessed: 42 } }; }, { retries: 2 },);Configuration
Section titled “Configuration”Configure the schedule expression and handler settings via ScheduleIntent:
infrastructure: { schedules: [ { id: 'daily-report', // matches src/crons/daily-report.ts schedule: { cron: '0 8 * * ? *' }, // 8 AM UTC daily timeout: 120, // 2 minute timeout memorySize: 512, }, { id: 'cleanup', schedule: { rate: '1 hour' }, // every hour }, ],}Retry Architecture
Section titled “Retry Architecture”- Application retries —
taskHandler({ retries: N })for transient errors within one invocation. - Infrastructure retries — EventBridge retries twice with exponential backoff if the Lambda returns an error.
Local Development
Section titled “Local Development”curl -X POST http://localhost:3000/_dev/invoke/cron/daily-reportThe dev server sends an EventBridge-like scheduled event to your handler.
Shared Logic (src/lib/)
Section titled “Shared Logic (src/lib/)”Code in src/lib/ is not deployed as separate Lambda functions. It’s shared code that gets bundled into whichever route or function imports it. The hot reload watcher covers the entire src/ directory, so changes to src/lib/ files automatically reload all handlers.
Best Practices
Section titled “Best Practices”- Put business logic in
src/lib/— validation, pricing, calculations - Put infrastructure clients in
src/lib/— database, email, storage wrappers - Keep handlers thin — route and function handlers should delegate to
src/lib/ - Type-safe imports — use standard TypeScript imports, no special syntax needed
Example: Shared Database Client
Section titled “Example: Shared Database Client”import { createClient } from '@venturekit/data';
export const db = createClient({ connectionString: process.env.DATABASE_URL,});
export async function getUser(id: string) { return db.query('SELECT * FROM users WHERE id = $1', [id]);}
export async function createOrder(userId: string, items: any[]) { return db.query( 'INSERT INTO orders (user_id, items) VALUES ($1, $2) RETURNING *', [userId, JSON.stringify(items)] );}// src/routes/users/[id]/get.ts — thin route handlerimport { handler } from '@venturekit/runtime';import { getUser } from '../../../lib/db.js';
export const main = handler({ fn: async (_body, ctx) => { const user = await getUser(ctx.pathParams.id); return { data: user }; },});// src/functions/process-orders.ts — thin function handlerimport { taskHandler } from '@venturekit/runtime';import { createOrder } from '../lib/db.js';
export const main = taskHandler<{ userId: string; items: any[] }>( async (event, _ctx, logger) => { const order = await createOrder(event.userId, event.items); logger.info('Order created', { orderId: order.id }); return { orderId: order.id }; },);Example: Shared Validation
Section titled “Example: Shared Validation”export function validateEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);}
export function validateOrderItems(items: any[]): { valid: boolean; error?: string } { if (!items || items.length === 0) return { valid: false, error: 'No items' }; if (items.length > 100) return { valid: false, error: 'Too many items' }; return { valid: true };}Both route handlers and standalone functions can import from src/lib/:
// Works in src/routes/orders/post.tsimport { validateOrderItems } from '../../lib/validation.js';
// Works in src/functions/process-orders.tsimport { validateOrderItems } from '../lib/validation.js';Directory Summary
Section titled “Directory Summary”| Directory | Convention | Deployment |
|---|---|---|
src/routes/ | {path}/{method}.ts | API Gateway + Lambda |
src/functions/ | {name}.ts | Lambda only (fn-{name}) |
src/queues/ | {name}.ts | SQS + Lambda (queue-{name}) |
src/crons/ | {name}.ts | EventBridge + Lambda (cron-{name}) |
src/lib/ | any .ts | Bundled into importers |
Related
Section titled “Related”- File-Based Routing — route handler conventions
- Lambda Invocation — calling functions from handlers
- Saga Pattern — distributed transactions using standalone functions