Skip to content

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.json

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.

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

The function name is derived from its file path under src/functions/:

FileFunction NameAWS Lambda Name
process-orders.tsprocess-orders{project}-{stage}-fn-process-orders
order/create-order.tsorder-create-order{project}-{stage}-fn-order-create-order
send-email.tssend-email{project}-{stage}-fn-send-email
import { invoke } from '@venturekit/runtime';
// Call a standalone function by name
const result = await invoke({ function: 'process-orders' }, {
payload: { orderId: 'abc-123', items: [...] },
});
// Fire-and-forget
await invoke({ function: 'send-email' }, {
payload: { to: 'user@example.com', template: 'welcome' },
mode: 'async',
});

During vk dev, standalone functions are discovered automatically and can be invoked via HTTP:

Terminal window
# Invoke a function locally
curl -X POST http://localhost:3000/_dev/invoke/process-orders \
-H "Content-Type: application/json" \
-d '{"orderId": "abc-123"}'
# List all discovered functions and routes
curl http://localhost:3000/_dev/functions

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' },
],
}

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.

FileQueue NameAWS Lambda Name
order-events.ts{project}-{stage}-order-events{project}-{stage}-queue-order-events
notifications.ts{project}-{stage}-notifications{project}-{stage}-queue-notifications
src/queues/order-events.ts
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 },
);

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)
},
],
}

Queue consumers have two layers of retries:

  1. Application retriestaskHandler({ retries: N }) retries within a single invocation for transient errors.
  2. Infrastructure retries — when the Lambda throws, SQS re-delivers the message after visibilityTimeout. After maxReceiveCount failures, the message moves to the dead-letter queue.
Terminal window
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.

Scheduled task Lambda functions triggered by EventBridge rules. Each file becomes a Lambda with an EventBridge schedule.

FileAWS Lambda NameEventBridge Rule
daily-report.ts{project}-{stage}-cron-daily-report{project}-{stage}-cron-daily-report
cleanup.ts{project}-{stage}-cron-cleanup{project}-{stage}-cron-cleanup
src/crons/daily-report.ts
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 },
);

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
},
],
}
  1. Application retriestaskHandler({ retries: N }) for transient errors within one invocation.
  2. Infrastructure retries — EventBridge retries twice with exponential backoff if the Lambda returns an error.
Terminal window
curl -X POST http://localhost:3000/_dev/invoke/cron/daily-report

The dev server sends an EventBridge-like scheduled event to your handler.

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.

  • 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
src/lib/db.ts
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 handler
import { 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 handler
import { 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 };
},
);
src/lib/validation.ts
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.ts
import { validateOrderItems } from '../../lib/validation.js';
// Works in src/functions/process-orders.ts
import { validateOrderItems } from '../lib/validation.js';
DirectoryConventionDeployment
src/routes/{path}/{method}.tsAPI Gateway + Lambda
src/functions/{name}.tsLambda only (fn-{name})
src/queues/{name}.tsSQS + Lambda (queue-{name})
src/crons/{name}.tsEventBridge + Lambda (cron-{name})
src/lib/any .tsBundled into importers