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 '@j0nathan-ll0yd/core'
import { defineApiHandler } from '@j0nathan-ll0yd/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 '@j0nathan-ll0yd/core'
import { getRequiredEnv } from '@j0nathan-ll0yd/env'
import { logError, logInfo } from '@j0nathan-ll0yd/observability'
import { defineApiHandler } from '@j0nathan-ll0yd/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 '@j0nathan-ll0yd/core'
import { exportToS3 } from '@j0nathan-ll0yd/aws'
import { getRequiredEnv } from '@j0nathan-ll0yd/env'
import { logInfo } from '@j0nathan-ll0yd/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 '@j0nathan-ll0yd/core'
import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda'
import { storeConnection } from '@j0nathan-ll0yd/aws'
import { logInfo } from '@j0nathan-ll0yd/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 '@j0nathan-ll0yd/database/orm'
import { DatabaseOperation, RequiresTable, withQueryMetrics } from '@j0nathan-ll0yd/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 '@j0nathan-ll0yd/core'
import { fetchWithTimeout } from '@j0nathan-ll0yd/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 '@j0nathan-ll0yd/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 = "../node_modules/@j0nathan-ll0yd/cli/modules/core" }
module "api_gateway" { source = "../node_modules/@j0nathan-ll0yd/cli/modules/api-gateway" }
module "database" { source = "../node_modules/@j0nathan-ll0yd/cli/modules/database/aurora-dsql" }
module "eventbridge" { source = "../node_modules/@j0nathan-ll0yd/cli/modules/eventbridge" }
module "storage_data" { source = "../node_modules/@j0nathan-ll0yd/cli/modules/storage" }
module "websocket_api" { source = "../node_modules/@j0nathan-ll0yd/cli/modules/websocket-api" }

module "lambda_health_sync_post" {
  source        = "../node_modules/@j0nathan-ll0yd/cli/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.