Vendor Encapsulation Policy
Quick Reference
- When to use: Every third-party library interaction
- Enforcement: ZERO TOLERANCE
- Impact if violated: CRITICAL - Breaks architecture, testability, and observability
The Rule
NEVER import third-party libraries directly in Lambda handlers or business logic. Always use @mantleframework/* package wrappers.
This applies to:
- AWS SDK (
@aws-sdk/*) - Use@mantleframework/aws - Drizzle ORM (
drizzle-orm) - Use@mantleframework/database - Better Auth (
better-auth) - Use@mantleframework/auth - AWS Lambda Powertools - Use
@mantleframework/observability - Any other third-party service integration
Wrapper Package Locations
| Library | Wrapper Package | What It Provides |
|---|---|---|
| AWS SDK | @mantleframework/aws | S3, DynamoDB, SNS, SQS, Lambda clients with X-Ray tracing |
| Drizzle ORM | @mantleframework/database | Database client, query metrics, @RequiresTable decorator |
| Drizzle operators | @mantleframework/database/orm | Re-exports of eq, and, sql, type utilities |
| Better Auth | @mantleframework/auth | Configured auth instance, session management |
| Powertools | @mantleframework/observability | Logger, tracer, metrics with consistent configuration |
| Middy | @mantleframework/security | Middleware chains with security defaults |
Examples
AWS SDK
FORBIDDEN
typescript
// Direct SDK imports in handlers
import {S3Client, PutObjectCommand} from '@aws-sdk/client-s3'
import {DynamoDBClient} from '@aws-sdk/client-dynamodb'
const s3 = new S3Client({})REQUIRED
typescript
// Vendor wrapper imports
import {getS3Client, getDynamoDBClient} from '@mantleframework/aws'Drizzle ORM
FORBIDDEN
typescript
// Direct Drizzle imports in Lambda handlers
import {drizzle} from 'drizzle-orm/postgres-js'
import {eq} from 'drizzle-orm'
import postgres from 'postgres'REQUIRED
typescript
// Framework API from @mantleframework/database
import {getDrizzleClient, withQueryMetrics, RequiresTable} from '@mantleframework/database'
// Drizzle operators and types from the /orm subpath
import {eq, and, sql} from '@mantleframework/database/orm'
import type {InferSelectModel, InsertModel} from '@mantleframework/database/orm'
// Schema definitions (only place drizzle-orm/pg-core is allowed)
import {pgTable, text, uuid, timestamp} from 'drizzle-orm/pg-core'Better Auth
FORBIDDEN
typescript
// Direct Better Auth usage
import {betterAuth} from 'better-auth'
const auth = betterAuth({...})REQUIRED
typescript
// Use configured auth instance
import {auth} from '@mantleframework/auth'
const session = await auth.api.getSession({...})Observability
FORBIDDEN
typescript
// Direct Powertools imports
import {Logger} from '@aws-lambda-powertools/logger'
import {Tracer} from '@aws-lambda-powertools/tracer'REQUIRED
typescript
// Wrapped observability
import {logger, tracer, metrics} from '@mantleframework/observability'Handler Usage
Handlers use define*Handler functions which provide observability automatically:
typescript
import { buildValidatedResponse } from '@mantleframework/core'
import { defineApiHandler } from '@mantleframework/validation'
import { z } from '@mantleframework/validation'
import { getDrizzleClient } from '@mantleframework/database'
import { putObject } from '@mantleframework/aws'
const ResponseSchema = z.object({ id: z.string() })
const api = defineApiHandler({ auth: 'bearer', operationName: 'CreateItem' })
export const handler = api(async ({ event, context, body }) => {
const db = getDrizzleClient()
// ... use db and putObject via @mantle wrappers
return buildValidatedResponse(context, 201, { id: 'abc' }, ResponseSchema)
})The Drizzle Import Convention
Drizzle access has a specific three-tier import convention:
@mantleframework/database - Framework API (getDrizzleClient, withQueryMetrics, RequiresTable)
@mantleframework/database/orm - Drizzle re-exports (operators: eq, and, sql; types: SelectModel)
drizzle-orm/pg-core - Schema definitions ONLY (pgTable, column types)This means:
- Handler files import from
@mantleframework/databaseand@mantleframework/database/orm - Entity query files import from
@mantleframework/databaseand@mantleframework/database/orm - Schema files are the ONLY place that imports from
drizzle-orm/pg-core
Wrapper Pattern
Each @mantleframework/* package follows this pattern:
typescript
// packages/aws/src/clients/s3.ts
import {S3Client} from '@aws-sdk/client-s3'
import {getRequiredEnv} from '@mantleframework/env'
let client: S3Client | null = null
/** Lazy-initialized S3 client with X-Ray tracing */
export function getS3Client(): S3Client {
if (!client) {
client = new S3Client({region: getRequiredEnv('AWS_REGION')})
}
return client
}Key aspects:
- Lazy initialization - Client created on first use, not at import time
- Environment detection - Uses
@mantleframework/envfor configuration - Singleton pattern - One client instance per Lambda execution environment
- Observability - X-Ray tracing and metrics built in
Rationale
- Testability - Mock
@mantleframework/awsinstead of@aws-sdk/client-s3; one mock point per service - Observability - X-Ray tracing, CloudWatch metrics, and structured logging applied consistently
- Configuration - Region, endpoints, and credentials managed in one place
- Migration safety - Swap underlying library without changing consumer code
- Consistency - Same patterns across all Lambda functions
Enforcement
| Method | Scope |
|---|---|
mantle check CLI | Validates all convention rules including vendor encapsulation |
| MCP validation server | Real-time convention checking with 21+ rules |
ESLint local-rules/enforce-powertools | Powertools imports |
ESLint local-rules/env-validation | Environment variable access |
ESLint no-raw-aws-sdk rule | Catches direct @aws-sdk/* imports in handler code |
| Code Review | All vendor libraries |
mantle generate permissions | Validates entity query patterns |
Testing
typescript
// Mock the @mantle wrapper, not the underlying SDK
vi.mock('@mantleframework/aws', () => ({
getS3Client: vi.fn(),
getDynamoDBClient: vi.fn()
}))
// For database tests, mock @mantleframework/database
vi.mock('@mantleframework/database', () => ({
getDrizzleClient: vi.fn(),
withQueryMetrics: vi.fn((fn) => fn)
}))Adding New Vendors
- Create wrapper in
packages/<vendor-name>/ - Follow the package structure convention (
build.config.ts,src/index.ts, etc.) - Implement lazy initialization pattern
- Add observability (tracing, metrics) where appropriate
- Export domain-specific functions
- Update this documentation
Related Patterns
- Import Organization -
@mantleframework/*import ordering - Naming Conventions - Package and wrapper naming
- Code Comments - Documenting wrapper functions
ZERO TOLERANCE: Always use @mantleframework/* wrappers for third-party libraries. Direct imports of AWS SDK, Drizzle, Better Auth, or Powertools in handler code are never acceptable.