Skip to content

Examples

Real production patterns from Mantle instances. Each example links to the source file and highlights the key lines.

API Handler -- Simple GET

A health check endpoint with bearer auth and response validation.

Source: mantle-LifegamesPortal/src/lambdas/api/health/ping.get.ts

typescript
import { buildValidatedResponse } from '@mantleframework/core'
import { defineApiHandler } from '@mantleframework/validation'
import { statusResponseSchema } from '../../../schemas/shared.js'

const api = defineApiHandler({ auth: 'bearer' })
export const handler = api(async ({ context }) => {
  return buildValidatedResponse(context, 200, { status: 'ok' }, statusResponseSchema)
})

Key points: defineApiHandler with auth: 'bearer' validates the static bearer token automatically. buildValidatedResponse checks the response body against the Zod schema before returning.


API Handler -- POST with Schema Validation

Adds a book by ASIN, fetching metadata from an external API and triggering an async export.

Source: mantle-LifegamesPortal/src/lambdas/api/books/add.post.ts

typescript
import { buildValidatedResponse, defineLambda, emitEvent } from '@mantleframework/core'
import { getRequiredEnv } from '@mantleframework/env'
import { logError, logInfo } from '@mantleframework/observability'
import { defineApiHandler } from '@mantleframework/validation'
import { findByAsin, insertBook, upsertUserBook } from '../../../entities/queries/bookQueries.js'
import { addBookRequestSchema, addBookResponseSchema } from '../../../schemas/books.js'
import { fetchBookData } from '../../../services/scrapingDogService.js'

defineLambda({ env: ['EVENT_BUS_NAME'] })

const api = defineApiHandler({ schema: addBookRequestSchema, auth: 'bearer' })
export const handler = api(async ({ context, body }) => {
  let created = false
  let book = await findByAsin(body.asin)

  if (!book) {
    const scrapingDogApiKey = getRequiredEnv('SCRAPINGDOG_API_KEY')
    const bookDataResult = await fetchBookData(body.asin, scrapingDogApiKey)
    if (!bookDataResult.ok) {
      logError('Failed to fetch book data', { asin: body.asin, error: bookDataResult.error.message })
      throw bookDataResult.error
    }
    book = await insertBook(bookDataResult.value)
    created = true
  }

  const userBook = await upsertUserBook(body.asin, { status: 'pending', totalPages: book.pageCount ?? undefined })
  logInfo('Book added', { asin: body.asin, created })

  await emitEvent({ detailType: 'ExportBooks', detail: { source: 'books-api' } })

  const response = { book: assembleBookResponse(book, userBook, body.asin), created, timestamp: new Date().toISOString() }
  return buildValidatedResponse(context, 200, response, addBookResponseSchema)
})

Key points: defineLambda({ env: [...] }) declares env vars needed at build time. schema: addBookRequestSchema validates and types body automatically. The handler uses the Result pattern to handle the external API call, then fires an EventBridge event for async export.


EventBridge Handler

Exports health data to S3 in response to EventBridge events with detail-type routing.

Source: mantle-LifegamesPortal/src/lambdas/eventbridge/ExportHealthData/index.ts

typescript
import { defineEventBridgeHandler, defineLambda, emitEvent } from '@mantleframework/core'
import { exportToS3 } from '@mantleframework/aws'
import { getRequiredEnv } from '@mantleframework/env'
import { logInfo } from '@mantleframework/observability'
import { getTodayQuantities, getTodaySleep, getTodayWorkouts } from '../../../entities/queries/exportHealthQueries.js'
import { transformQuantities, transformSleep, transformWorkouts } from '../../../services/healthExportService.js'

defineLambda({ env: ['EVENT_BUS_NAME'] })

const eb = defineEventBridgeHandler({
  detailTypes: ['ExportHealth', 'ExportSleep', 'ExportWorkouts'],
  detailSchema: HealthExportDetailSchema,
  timeout: 60
})
export const handler = eb(async ({ detailType, detail }) => {
  const bucket = getRequiredEnv('DATA_BUCKET')
  const today = detail.date ?? todayLocalDateString()
  const generatedAt = new Date().toISOString()

  if (detailType === 'ExportHealth') {
    const quantities = await getTodayQuantities(today)
    const healthJson = transformQuantities(today, generatedAt, quantities)
    await exportToS3({ bucket, key: 'health.json', data: healthJson })
  }
  // ... similar for ExportSleep, ExportWorkouts

  logInfo('Health data exported', { detailType, date: today })
  await emitEvent({ detailType: 'BroadcastUpdate', detail: { resource: 'health' } })

  return { date: today, detailType }
})

Key points: detailTypes array controls which EventBridge events trigger this Lambda. detailSchema validates the event detail payload. exportToS3 writes JSON to S3 with proper content type and cache headers. The handler chains a follow-up event via emitEvent.


WebSocket Handler

Stores a WebSocket connection in DynamoDB on $connect.

Source: mantle-LifegamesPortal/src/lambdas/websocket/connect.ts

typescript
import { defineLambda, defineWebSocketHandler } from '@mantleframework/core'
import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda'
import { storeConnection } from '@mantleframework/aws'
import { logInfo } from '@mantleframework/observability'

defineLambda({ env: ['CONNECTIONS_TABLE_NAME'] })

const ws = defineWebSocketHandler({ routeKey: '$connect', operationName: 'WsConnect' })
export const handler: APIGatewayProxyWebsocketHandlerV2 = ws(async ({ connectionId }) => {
  await storeConnection(connectionId, undefined, 7200)
  logInfo('WebSocket client connected', { connectionId })
  return { statusCode: 200 }
})

Key points: routeKey maps to WebSocket API Gateway routes ($connect, $disconnect, $default). The handler receives connectionId from the factory. WebSocket config is declared in mantle.config.ts under the websocket key.


Entity Queries

Database access using the entity query pattern with @RequiresTable decorators and withQueryMetrics wrapping.

Source: mantle-LifegamesPortal/src/entities/queries/bookQueries.ts

typescript
import { eq } from '@mantleframework/database/orm'
import { DatabaseOperation, RequiresTable, withQueryMetrics } from '@mantleframework/database'
import { getDb } from '../../db/client.js'
import { books, userBooks } from '../../db/schema.js'

class BookQueries {
  @RequiresTable([{ table: 'books', operations: [DatabaseOperation.Select] }])
  static findByAsin(asin: string) {
    return withQueryMetrics('Books.findByAsin', async () => {
      const db = await getDb()
      const result = await db.select().from(books).where(eq(books.asin, asin)).limit(1)
      return result[0] ?? null
    }, { logInput: { asin } })
  }

  @RequiresTable([{ table: 'books', operations: [DatabaseOperation.Select, DatabaseOperation.Insert] }])
  static insertBook(data: { asin: string; title: string; /* ... */ }) {
    return withQueryMetrics('Books.insertBook', async () => {
      const db = await getDb()
      const result = await db.insert(books).values(data).returning()
      return result[0]!
    }, { logInput: { asin: data.asin, title: data.title } })
  }
}

// Export bound functions for direct import
export const findByAsin = BookQueries.findByAsin.bind(BookQueries)
export const insertBook = BookQueries.insertBook.bind(BookQueries)

Key points: Static class with @RequiresTable Stage 3 decorators declaring table permissions. withQueryMetrics adds X-Ray tracing, CloudWatch metrics, error handling, and automatic OCC retry. Bound exports allow handlers to import individual functions. The CLI extracts @RequiresTable declarations to generate per-Lambda PostgreSQL roles.


Result Type in Services

Service functions return Result<T, E> for expected failures instead of throwing.

Source: mantle-LifegamesPortal/src/services/scrapingDogService.ts

typescript
import { err, ok, type Result } from '@mantleframework/core'
import { fetchWithTimeout } from '@mantleframework/resilience'

export async function fetchBookData(asin: string, apiKey: string): Promise<Result<ScrapedBookData>> {
  try {
    const response = await fetchWithTimeout(url, { timeoutMs: 20_000 })
    if (!response.ok) {
      return err(new Error(`ScrapingDog API error: ${response.status}`))
    }
    const data = (await response.json()) as ScrapingDogResponse
    return ok({ asin, title: cleanTitle(parseTitle(data.title)), /* ... */ })
  } catch (error) {
    return err(error instanceof Error ? error : new Error(String(error)))
  }
}

The caller decides how to handle the failure -- see the Error Handling guide for the unwrap pattern.


Configuration

Instance configuration via defineConfig in mantle.config.ts.

Source: mantle-LifegamesPortal/mantle.config.ts

typescript
import { defineConfig } from '@mantleframework/core'

export default defineConfig({
  name: 'lifegames-portal',
  database: { provider: 'aurora-dsql' },
  eventbridge: { bus: 'lifegames-portal' },
  storage: [{
    name: 'data',
    cloudfront: true,
    cloudfrontPriceClass: 'PriceClass_100',
    corsOrigins: ['https://j0nathan-ll0yd.github.io', 'https://jonathanlloyd.me'],
    corsMethods: ['GET', 'HEAD'],
  }],
  websocket: {
    routeSelectionExpression: '$request.body.action',
    stageName: 'live',
    throttlingBurstLimit: 50,
    throttlingRateLimit: 25,
  },
  dynamodb: [{
    name: 'connections',
    hashKey: 'connectionId',
    attributes: [{ name: 'connectionId', type: 'S' }],
    ttlAttribute: 'ttl',
  }],
})

Key points: defineConfig declares all infrastructure resources. The CLI reads this to generate Terraform modules, environment variables, and IAM policies. Storage with cloudfront: true creates an S3 bucket with a CloudFront distribution. The websocket key generates a WebSocket API Gateway with the specified routes and throttling.


Infrastructure

Instances source Terraform modules from the Mantle framework. All .tf files are generated by mantle generate infra.

hcl
# Generated by mantle generate infra
module "core" { source = "../../../mantle/modules/core" }
module "api_gateway" { source = "../../../mantle/modules/api-gateway" }
module "database" { source = "../../../mantle/modules/database/aurora-dsql" }
module "eventbridge" { source = "../../../mantle/modules/eventbridge" }
module "storage_data" { source = "../../../mantle/modules/storage" }
module "websocket_api" { source = "../../../mantle/modules/websocket-api" }

module "lambda_health_sync_post" {
  source        = "../../../mantle/modules/lambda"
  function_name = "${module.core.name_prefix}-HealthSyncPost"
  handler       = "index.handler"
  # ... environment, IAM policies, triggers auto-generated
}

Key points: One module block per Lambda. The module handles IAM roles, log groups, environment variables, and trigger configuration. Never hand-write .tf files that the CLI can generate -- see C21 for the convention.