
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
AWS Patterns Kit - TypeScript utility library with AWS SDK clients, Lambda helpers, error handling, and data validation
AWS Patterns Kit - A utility library for AWS patterns
A TypeScript utility library that provides helper functions for common AWS patterns and operations. The library is organized into modular categories for easy discoverability and usage.
npm install awpaki
The library uses environment variables for AWS client configuration and Lambda metadata. Most are automatically set by AWS Lambda runtime.
| Variable | Description | Required | Default |
|---|---|---|---|
AWS_REGION | AWS region for all clients (e.g., us-east-1) | Yes* | - |
AWS_DEFAULT_REGION | Fallback region if AWS_REGION is not set | No | - |
AWS_ENDPOINT_URL | Global endpoint override for all AWS services | No | - |
AWS_ENDPOINT_URL_DYNAMODB | DynamoDB-specific endpoint override | No | AWS_ENDPOINT_URL |
AWS_ENDPOINT_URL_S3 | S3-specific endpoint override | No | AWS_ENDPOINT_URL |
AWS_ENDPOINT_URL_SQS | SQS-specific endpoint override | No | AWS_ENDPOINT_URL |
AWS_ENDPOINT_URL_LAMBDA | Lambda-specific endpoint override | No | AWS_ENDPOINT_URL |
AWS_ENDPOINT_URL_SNS | SNS-specific endpoint override | No | AWS_ENDPOINT_URL |
* AWS_REGION is automatically set by AWS Lambda runtime. Only required when running locally.
These variables are automatically set by AWS Lambda and used for enhanced error responses:
| Variable | Description | Used For |
|---|---|---|
AWS_LAMBDA_FUNCTION_NAME | Name of the Lambda function | Error metadata |
AWS_LAMBDA_LOG_STREAM_NAME | CloudWatch log stream name | Error tracing |
AWS_EXECUTION_ENV | Lambda execution environment (e.g., AWS_Lambda_nodejs20.x) | Error metadata |
# .env file for local development
AWS_REGION=us-east-1
AWS_ENDPOINT_URL=http://localhost:4566
# Or service-specific endpoints
AWS_ENDPOINT_URL_DYNAMODB=http://localhost:4566
AWS_ENDPOINT_URL_S3=http://localhost:4566
AWS_ENDPOINT_URL_SQS=http://localhost:4566
In production, AWS Lambda automatically sets:
AWS_REGION - From the Lambda's deployed regionAWS_LAMBDA_FUNCTION_NAME - From the function configurationAWS_LAMBDA_LOG_STREAM_NAME - Unique per invocationAWS_EXECUTION_ENV - Lambda runtime informationNo additional configuration is needed when running in AWS Lambda.
The library is organized into categories for better organization:
// Option 1: Import from root (recommended)
import { parseJsonBody } from 'awpaki';
// Option 2: Import from specific category
import { parseJsonBody } from 'awpaki/parsers';
// Option 3: Import entire category as namespace
import * as parsers from 'awpaki/parsers';
AWS provides official TypeScript types for all Lambda handlers via @types/aws-lambda. Use these types instead of manually typing events and return values:
import {
// API Gateway
APIGatewayProxyHandler, // event: APIGatewayProxyEvent β APIGatewayProxyResult
APIGatewayProxyHandlerV2, // HTTP API (v2)
// SQS
SQSHandler, // event: SQSEvent β SQSBatchResponse | void
// SNS
SNSHandler, // event: SNSEvent β void
// DynamoDB Streams
DynamoDBStreamHandler, // event: DynamoDBStreamEvent β DynamoDBBatchResponse | void
// S3
S3Handler, // event: S3Event β void
S3BatchHandler, // S3 Batch Operations
// EventBridge
EventBridgeHandler, // Generic EventBridge handler
ScheduledHandler, // CloudWatch Events/cron
// Others
ALBHandler, // Application Load Balancer
CloudFrontRequestHandler, // CloudFront
// ... many more available
} from 'aws-lambda';
// Usage example
export const myHandler: SQSHandler = async (event, context) => {
// event is typed as SQSEvent
// context is typed as Context
// return type is SQSBatchResponse | void
};
Benefits:
Quick Reference:
| Handler Type | Event Type | Return Type | Use Case |
|---|---|---|---|
APIGatewayProxyHandler | APIGatewayProxyEvent | APIGatewayProxyResult | REST API |
APIGatewayProxyHandlerV2 | APIGatewayProxyEventV2 | APIGatewayProxyResultV2 | HTTP API (v2) |
AppSyncResolverHandler<TArgs, TResult> | AppSyncResolverEvent<TArgs> | TResult | Promise<TResult> | AppSync GraphQL |
SQSHandler | SQSEvent | SQSBatchResponse | void | Message queues |
SNSHandler | SNSEvent | void | Pub/sub notifications |
DynamoDBStreamHandler | DynamoDBStreamEvent | DynamoDBBatchResponse | void | Database streams |
S3Handler | S3Event | void | Object storage events |
EventBridgeHandler<T, D, R> | EventBridgeEvent<T, D> | R | Custom events |
ScheduledHandler<T> | ScheduledEvent<T> | void | Cron/scheduled |
ALBHandler | ALBEvent | ALBResult | Load balancer |
Response Types:
// API Gateway - Must return proper structure
APIGatewayProxyResult: {
statusCode: number;
headers?: { [key: string]: string };
body: string; // Must be JSON stringified
}
// SQS - Optional batch failure reporting
SQSBatchResponse: {
batchItemFailures: Array<{ itemIdentifier: string }>;
}
// DynamoDB - Optional batch failure reporting
DynamoDBBatchResponse: {
batchItemFailures: Array<{ itemIdentifier: string }>;
}
// SNS, S3, EventBridge - No return value (void)
This example demonstrates all the key patterns from the library:
import {
// Logging
logApiGatewayEvent,
// Parameter extraction & validation
extractEventParams,
ParameterType,
// Error handling
handleApiGatewayError,
NotFound,
HttpStatus,
HttpErrorStatus,
// Type safety
HttpError
} from 'awpaki';
import { APIGatewayProxyHandler } from 'aws-lambda';
/**
* Example: Update user profile
* GET /users/{userId}
* PUT /users/{userId}
*/
export const handler: APIGatewayProxyHandler = async (event, context) => {
// 1οΈβ£ Log incoming event for debugging
// AWS will filter logs based on Lambda configuration (info/debug)
logApiGatewayEvent(event, context);
try {
// 2οΈβ£ Extract and validate all parameters with type safety
const params = extractEventParams({
// Path parameters (from URL)
pathParameters: {
userId: {
label: 'User ID',
required: true,
expectedType: ParameterType.STRING,
statusCodeError: HttpErrorStatus.NOT_FOUND, // 404 if missing
},
},
// Headers (authentication, content-type, etc)
headers: {
authorization: {
label: 'Authorization',
required: true,
caseInsensitive: true, // Matches Authorization, authorization, AUTHORIZATION
statusCodeError: HttpErrorStatus.UNAUTHORIZED, // 401 if missing
},
'content-type': {
label: 'Content-Type',
default: 'application/json',
},
},
// Request body (for POST/PUT/PATCH)
body: {
name: {
label: 'Name',
required: true,
expectedType: ParameterType.STRING,
},
email: {
label: 'Email',
required: true,
expectedType: ParameterType.STRING,
decoder: (value: string) => {
// Custom validation/transformation
if (!value.includes('@')) {
throw new Error('Invalid email format');
}
return value.toLowerCase();
},
},
age: {
label: 'Age',
expectedType: ParameterType.NUMBER,
default: 18, // Optional with default
},
tags: {
label: 'Tags',
expectedType: ParameterType.ARRAY,
default: [],
},
},
// Query string parameters
queryStringParameters: {
includeDetails: {
label: 'Include Details',
expectedType: ParameterType.BOOLEAN,
default: false,
},
},
}, event);
// 3οΈβ£ Business logic with validated parameters
// All params are now type-safe and validated
const token = params.authorization.replace('Bearer ', '');
// Simulate database lookup
const existingUser = await getUserById(params.userId);
if (!existingUser) {
// Throw type-safe HTTP errors
throw new NotFound(`User ${params.userId} not found`);
}
// Update user
const updatedUser = await updateUser(params.userId, {
name: params.name,
email: params.email, // Already normalized by decoder
age: params.age,
tags: params.tags,
});
// 4οΈβ£ Return success response
return {
statusCode: HttpStatus.OK, // Type-safe status code
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
success: true,
data: params.includeDetails
? { ...updatedUser, metadata: { updatedAt: new Date().toISOString() } }
: updatedUser,
}),
};
} catch (error) {
// 5οΈβ£ Centralized error handling
// Converts HttpError to proper API Gateway response
// Re-throws non-HTTP errors for Lambda retry/DLQ
return handleApiGatewayError(error);
}
};
// Mock functions (replace with your actual implementation)
async function getUserById(userId: string) {
// Your database logic
return { id: userId, name: 'John Doe' };
}
async function updateUser(userId: string, data: any) {
// Your database logic
return { id: userId, ...data };
}
Cada tipo de evento Lambda tem um logger especΓfico que extrai informaΓ§Γ΅es relevantes:
| Logger Function | Event Type | Info Logged | Debug Logged |
|---|---|---|---|
logApiGatewayEvent(event, context) | API Gateway | HTTP method, path, user agent | All headers, query params, body |
logSqsEvent(event, context) | SQS | Queue name, record count | Full message bodies |
logSnsEvent(event, context) | SNS | Topic ARN, subject, record count | Full message content |
logEventBridgeEvent(event, context) | EventBridge | Source, detail-type, account | Full event detail |
logS3Event(event, context) | S3 | Bucket, object key, event type | Object size, etag |
logDynamoDBStreamEvent(event, context) | DynamoDB Streams | Table name, event types, keys | Full old/new images |
logAppSyncEvent(event, context) | AppSync | Operation, field name, identity | Full arguments, source, headers |
Uso:
import { logSqsEvent, logSnsEvent, logS3Event } from 'awpaki';
// SQS Handler
export const sqsHandler: SQSHandler = async (event, context) => {
logSqsEvent(event, context);
// Logs: "SQS Event: queue=my-queue records=10"
// ...
};
// SNS Handler
export const snsHandler: SNSHandler = async (event, context) => {
logSnsEvent(event, context);
// Logs: "SNS Event: topic=arn:aws:sns:...:my-topic subject=Alert records=1"
// ...
};
// S3 Handler
export const s3Handler: S3Handler = async (event, context) => {
logS3Event(event, context);
// Logs: "S3 Event: bucket=my-bucket key=folder/file.jpg eventName=ObjectCreated:Put"
// ...
};
Cada tipo de trigger precisa de um error handler especΓfico:
| Error Handler | Event Type | Return Type | Comportamento |
|---|---|---|---|
handleApiGatewayError(error) | API Gateway V1 (REST API) | APIGatewayProxyResult | Retorna response HTTP com statusCode |
handleApiGatewayErrorV2(error, cookies?) | API Gateway V2 (HTTP API) | APIGatewayProxyResultV2 | Retorna response V2 com suporte a cookies |
handleAppSyncError(error) | AppSync | never | Loga e re-lanΓ§a erro (GraphQL formata) |
handleSqsError(error) | SQS | void | Re-lanΓ§a erro para retry/DLQ |
handleSnsError(error) | SNS | void | Re-lanΓ§a erro para retry/DLQ |
handleEventBridgeError(error) | EventBridge | void | Re-lanΓ§a erro para retry/DLQ |
handleS3Error(error) | S3 | void | Re-lanΓ§a erro para retry/DLQ |
handleDynamoDBStreamError(error) | DynamoDB Streams | void | Re-lanΓ§a erro para retry/DLQ |
handleGenericError(error) | Qualquer | void | Alias genΓ©rico (mesma lΓ³gica) |
DiferenΓ§as:
HttpError em response HTTP formatado. NΓ£o re-lanΓ§a.errors.Uso:
import {
handleApiGatewayErrorV2, // NEW: For V2 (HTTP API)
handleSqsError,
handleDynamoDBStreamError,
BadRequest,
Unauthorized
} from 'awpaki';
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
// API Gateway V2 Handler (HTTP API) with cookies support
export const apiV2Handler: APIGatewayProxyHandlerV2 = async (event, context) => {
try {
// Your business logic
const user = await authenticateUser(event.headers.authorization);
return {
statusCode: 200,
body: JSON.stringify({ user }),
cookies: [`session=${user.sessionId}; HttpOnly; Secure`]
};
} catch (error) {
// Clear cookies on error if needed
const cookies = error instanceof Unauthorized
? ['session=; Max-Age=0']
: undefined;
return handleApiGatewayErrorV2(error, cookies);
}
};
// SQS Handler
export const sqsHandler: SQSHandler = async (event, context) => {
try {
// Process messages
for (const record of event.Records) {
const body = JSON.parse(record.body);
await processMessage(body);
}
} catch (error) {
return handleSqsError(error); // Re-throws for retry
}
};
// DynamoDB Stream Handler com batch failures
export const streamHandler: DynamoDBStreamHandler = async (event, context) => {
const batchItemFailures: { itemIdentifier: string }[] = [];
for (const record of event.Records) {
try {
await processRecord(record);
} catch (error) {
console.error('Failed record:', record.eventID, error);
batchItemFailures.push({
itemIdentifier: record.dynamodb?.SequenceNumber || ''
});
}
}
// Return failed items for retry (type-safe)
return { batchItemFailures };
};
ObservaΓ§Γ£o: Todos os handlers SQS/SNS/EventBridge/S3/DynamoDB sΓ£o aliases de handleGenericError. Use o que for mais semΓ’ntico para seu caso.
AppSync Lambda resolvers receive GraphQL context and return typed results. The library provides native AppSync support with AppSyncEventSchema:
import {
extractEventParams,
logAppSyncEvent,
handleAppSyncError,
AppSyncEventSchema,
ParameterType,
NotFound,
HttpErrorStatus,
validEmail,
trimmedString
} from 'awpaki';
import { AppSyncResolverHandler } from 'aws-lambda';
// Define your GraphQL types
interface GetUserArgs {
id: string;
}
interface User {
id: string;
name: string;
email: string;
}
// AppSync resolver handler with typed arguments and result
export const getUserResolver: AppSyncResolverHandler<GetUserArgs, User> = async (event, context) => {
// Log AppSync event with operation details
logAppSyncEvent(event, context);
try {
// Native AppSync schema - extracts from arguments, identity, and identity.claims
const params = extractEventParams<{
id: string;
sub: string;
email: string;
}>({
// GraphQL arguments
arguments: {
id: {
label: 'User ID',
required: true,
expectedType: ParameterType.STRING,
statusCodeError: HttpErrorStatus.BAD_REQUEST,
},
},
// Cognito identity with nested claims
identity: {
sub: {
label: 'Caller ID',
required: true,
statusCodeError: HttpErrorStatus.UNAUTHORIZED,
},
claims: {
email: {
label: 'Caller Email',
required: true,
decoder: validEmail,
},
},
},
} as AppSyncEventSchema, event);
// Authorization check
if (params.sub !== params.id) {
throw new NotFound('Unauthorized to access this user');
}
// Fetch user from database
const user = await getUserById(params.id);
if (!user) {
throw new NotFound(`User ${params.id} not found`);
}
return {
id: user.id,
name: user.name,
email: user.email,
};
} catch (error) {
// Logs HttpError and re-throws for GraphQL to format
return handleAppSyncError(error);
}
};
// Mutation example with nested input validation
interface CreateUserArgs {
input: {
name: string;
email: string;
};
}
export const createUserResolver: AppSyncResolverHandler<CreateUserArgs, User> = async (event, context) => {
logAppSyncEvent(event, context);
try {
// For mutations with nested input, map to arguments
const params = extractEventParams<{ name: string; email: string }>({
arguments: {
name: {
label: 'Name',
required: true,
expectedType: ParameterType.STRING,
decoder: trimmedString,
},
email: {
label: 'Email',
required: true,
expectedType: ParameterType.STRING,
decoder: validEmail,
},
},
} as AppSyncEventSchema, {
// Flatten input to arguments
...event,
arguments: event.arguments.input || {},
});
// Create user with validated data
const user = await createUser({
name: params.name,
email: params.email,
});
return user;
} catch (error) {
throw error; // AppSync will format as GraphQL error
}
};
// Nested resolver example (field resolver)
interface Post {
id: string;
title: string;
authorId: string;
}
interface PostAuthorArgs {
// No arguments for this field
}
// Resolver for Post.author field
export const postAuthorResolver: AppSyncResolverHandler<PostAuthorArgs, User> = async (event) => {
// event.source contains the parent Post object
const post = event.source as Post;
console.debug('Resolving author for post', { postId: post.id, authorId: post.authorId });
const author = await getUserById(post.authorId);
if (!author) {
throw new NotFound(`Author ${post.authorId} not found`);
}
return author;
};
// Mock functions (replace with your actual implementation)
async function getUserById(id: string): Promise<User | null> {
// Your database logic
return null;
}
async function createUser(data: { name: string; email: string }): Promise<User> {
// Your database logic
return { id: '123', ...data };
}
AppSync Event Structure:
{
arguments: TArgs, // GraphQL query/mutation arguments
identity: { // Caller identity
sub: string, // User ID (Cognito)
username?: string,
claims?: Record<string, any>,
sourceIp?: string[],
},
source: TSource, // Parent object (for nested resolvers)
request: {
headers: Record<string, string>,
},
info: {
fieldName: string, // GraphQL field being resolved
parentTypeName: string, // Parent type (Query, Mutation, etc)
variables: Record<string, any>,
},
prev: { // Previous resolver result (pipeline)
result: any,
},
stash: Record<string, any>, // State shared across pipeline resolvers
}
AWS-native logging for all Lambda trigger types. Emits structured logs at appropriate levels:
AWS Lambda automatically filters logs based on your configuration. No environment variables needed in your code.
import {
logApiGatewayEvent,
logApiGatewayEventV2, // NEW: For API Gateway V2 (HTTP API)
logSqsEvent,
logSnsEvent,
logEventBridgeEvent,
logS3Event,
logDynamoDBStreamEvent
} from 'awpaki';
import {
APIGatewayProxyHandler,
APIGatewayProxyHandlerV2, // NEW: For V2
SQSHandler,
SNSHandler,
EventBridgeHandler,
S3Handler,
DynamoDBStreamHandler
} from 'aws-lambda';
// API Gateway V1 (REST API) - Logs request metadata + headers (debug)
export const apiHandler: APIGatewayProxyHandler = async (event, context) => {
logApiGatewayEvent(event, context);
// Info: { httpMethod, path, stage, sourceIp, requestId }
// Debug: Full headers object
};
// API Gateway V2 (HTTP API with Payload Format 2.0) - NEW!
export const apiHandlerV2: APIGatewayProxyHandlerV2 = async (event, context) => {
logApiGatewayEventV2(event, context);
// Info: { httpMethod, path, routeKey, stage, sourceIp, cookies, requestId }
// Debug: Full headers object
};
// SQS - Logs record count + truncated body (info), full body (debug)
export const sqsHandler: SQSHandler = async (event, context) => {
logSqsEvent(event, context);
// Info: { recordCount, messageIds, body: '...first 100 chars...' }
// Debug: { body: 'full message', receiptHandle }
};
// SNS - Logs topic + truncated message (info), full message (debug)
export const snsHandler: SNSHandler = async (event, context) => {
logSnsEvent(event, context);
// Info: { topicArn, subject, message: '...first 100 chars...' }
// Debug: { message: 'full message content' }
};
// EventBridge - Logs event metadata + detail keys (info), full detail (debug)
export const eventBridgeHandler: EventBridgeHandler<string, any, void> = async (event, context) => {
logEventBridgeEvent(event, context);
// Info: { source, detailType, detailKeys: 'key1, key2, key3' }
// Debug: { detail: { full detail object } }
};
// S3 - Logs object metadata (info only, simple enough)
export const s3Handler: S3Handler = async (event, context) => {
logS3Event(event, context);
// Info: { bucketName, objectKey, objectSize, eventName }
};
// DynamoDB - Logs keys as strings (info), full objects (debug)
export const dynamoHandler: DynamoDBStreamHandler = async (event, context) => {
logDynamoDBStreamEvent(event, context);
// Info: { eventName, keys: 'id, email', newImageKeys: 'id, name, email' }
// Debug: { Keys: { full object }, NewImage: { full object } }
}
// Add custom metadata to any logger
logApiGatewayEvent(event, context, {
additionalData: {
customField: 'customValue',
correlationId: event.headers['x-correlation-id'],
},
});
CloudWatch Configuration:
Configure log filtering in your Lambda/CloudWatch, not in code:
# serverless.yml or SAM template
functions:
myFunction:
environment:
# AWS uses this for log filtering (not read by awpaki)
AWS_LAMBDA_LOG_LEVEL: DEBUG # or INFO (default)
Parses a JSON stringified body with support for null handling, default values, and validation.
import { parseJsonBody, BadRequest } from 'awpaki';
// Basic parsing
const user = parseJsonBody<User>('{"name": "John", "age": 30}');
// Body is required by default
try {
const body = parseJsonBody<Body>(event.body);
} catch (error) {
if (error instanceof BadRequest) {
return error.toLambdaResponse();
}
}
// Make body optional with default value
const data = parseJsonBody<Data>(event.body, { defaultValue: { count: 0 } });
Extract and validate parameters from AWS Lambda events with comprehensive validation and multiple error collection.
import { extractEventParams, UnprocessableEntity } from 'awpaki';
// Define schema
const schema = {
pathParameters: {
id: {
label: 'User ID',
required: true,
expectedType: 'string'
}
},
body: {
email: {
label: 'Email',
required: true,
expectedType: 'string'
},
age: {
label: 'Age',
expectedType: 'number',
default: 18
}
},
headers: {
authorization: {
label: 'Authorization',
required: true,
caseInsensitive: true,
statusCodeError: 401
}
}
};
// Extract and validate
try {
const params = extractEventParams<{
id: string;
email: string;
age: number;
authorization: string;
}>(schema, event);
} catch (error) {
if (error instanceof UnprocessableEntity) {
// Multiple errors collected in errors object
console.log(error.errors);
// { 'body.email': 'Email is required', 'body.age': 'Age must be a number' }
return error.toLambdaResponse();
}
}
Comprehensive HTTP error classes with Lambda integration and multiple error support.
import {
BadRequest,
Unauthorized,
UnprocessableEntity,
NotFound
} from 'awpaki';
// Single error
throw new BadRequest('Invalid input');
// Multiple validation errors
throw new UnprocessableEntity('Validation failed', {
email: 'Invalid email format',
age: 'Must be 18 or older',
password: 'Must be at least 8 characters'
});
// With Lambda response
try {
// your code
} catch (error) {
if (error instanceof HttpError) {
return error.toLambdaResponse();
// Returns proper API Gateway response with status code
}
}
Decoders are validation and transformation functions that work with extractEventParams to ensure parameter quality. They validate inputs and transform them into the expected format:
import {
extractEventParams,
ParameterType,
validEmail,
trimmedString,
positiveInteger,
createEnum,
stringToBoolean,
optionalTrimmedString
} from 'awpaki';
import { APIGatewayProxyHandler } from 'aws-lambda';
export const handler: APIGatewayProxyHandler = async (event) => {
const params = extractEventParams({
body: {
email: {
label: 'Email',
required: true,
expectedType: ParameterType.STRING,
decoder: validEmail, // Validates format and normalizes to lowercase
},
name: {
label: 'Name',
required: true,
expectedType: ParameterType.STRING,
decoder: trimmedString, // Removes whitespace and validates non-empty
},
age: {
label: 'Age',
required: true,
expectedType: ParameterType.NUMBER,
decoder: positiveInteger, // Ensures positive integer
},
status: {
label: 'Status',
required: true,
expectedType: ParameterType.STRING,
decoder: createEnum(['active', 'inactive', 'pending']), // Only allows specific values
},
receiveNewsletter: {
label: 'Receive Newsletter',
expectedType: ParameterType.BOOLEAN,
default: false,
decoder: stringToBoolean, // Converts "true"/"false"/"1"/"0"/"yes"/"no" to boolean
},
bio: {
label: 'Bio',
expectedType: ParameterType.STRING,
default: '',
decoder: optionalTrimmedString('No bio provided'), // Returns default for non-string
},
},
}, event);
// params.email is now validated and lowercase
// params.name is trimmed with no extra spaces
// params.age is a positive integer
// params.status is one of: 'active', 'inactive', 'pending'
// params.receiveNewsletter is true/false boolean
// params.bio is trimmed string or default value
};
Available Decoders:
String Decoders:
trimmedString(value) - Removes whitespace, validates non-emptytrimmedLowerString(value) - Trims and converts to lowercasealphanumericId(value) - Validates alphanumeric with hyphens/underscores, converts to lowercasevalidEmail(value) - Validates email format, converts to lowercaseNumber Decoders:
positiveInteger(value) - Converts to integer, validates > 0limitedInteger(min?, max?)(value) - Validates integer within range (default 1-1000)JSON Decoders:
urlEncodedJson(value) - Decodes URL-encoded JSON stringjsonString(value) - Parses JSON stringEnum Decoder:
createEnum(validValues)(value) - Validates value is in allowed list, normalizes to lowercaseArray Decoder:
stringArray(value) - Filters array to non-empty stringsBoolean Decoder:
stringToBoolean(value) - Converts "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off" to booleanDate Decoder:
isoDateString(value) - Validates ISO date format, normalizes to ISO stringOptional Decoders:
optionalTrimmedString(defaultValue?)(value) - Returns trimmed string or default (default: '')optionalInteger(defaultValue?)(value) - Returns integer or default (default: 0)Examples:
// Email validation and normalization
decoder: validEmail
// Input: "USER@EXAMPLE.COM" β Output: "user@example.com"
// Trim and validate non-empty
decoder: trimmedString
// Input: " hello " β Output: "hello"
// Integer range validation
decoder: limitedInteger(1, 100)
// Input: "50" β Output: 50
// Input: "150" β throws "Must be a number between 1 and 100"
// Enum validation
decoder: createEnum(['admin', 'user', 'guest'])
// Input: "ADMIN" β Output: "admin"
// Input: "invalid" β throws "Must be one of: admin, user, guest"
// Boolean conversion
decoder: stringToBoolean
// Input: "yes" β Output: true
// Input: "0" β Output: false
// Input: "maybe" β throws error
// Optional with default
decoder: optionalTrimmedString('N/A')
// Input: null β Output: "N/A"
// Input: " text " β Output: "text"
parseJsonBody<T>(body: string | null | undefined, options?: ParseJsonBodyOptions<T>): TParses a JSON stringified body with enhanced null handling and validation.
Type Parameters:
T - The expected type of the parsed objectParameters:
body: string | null | undefined - The stringified JSON body to parseoptions?: ParseJsonBodyOptions<T> - Optional configuration
defaultValue?: T - Default value when body is empty (makes body optional)Returns:
T - The parsed object of type TThrows:
BadRequest - When body is invalid JSON or empty (unless defaultValue provided)Type-safe HTTP status codes for all standard HTTP responses:
import { HttpStatus, HttpErrorStatus, isValidHttpStatus, isValidHttpErrorStatus, getHttpStatusName } from 'awpaki';
// HttpStatus - All standard HTTP status codes (1xx, 2xx, 3xx, 4xx, 5xx)
return {
statusCode: HttpStatus.OK, // 200
body: JSON.stringify({ success: true })
};
return {
statusCode: HttpStatus.CREATED, // 201
body: JSON.stringify({ id: newId })
};
return {
statusCode: HttpStatus.NO_CONTENT, // 204
};
// HttpErrorStatus - Only error codes with mapped error classes
const schema = {
pathParameters: {
id: {
label: 'User ID',
required: true,
statusCodeError: HttpErrorStatus.NOT_FOUND // 404 - Type-safe!
}
},
headers: {
authorization: {
label: 'Authorization',
required: true,
statusCodeError: HttpErrorStatus.UNAUTHORIZED // 401
}
}
};
// Validation helpers
isValidHttpStatus(200); // true - validates all HTTP status codes
isValidHttpStatus(404); // true
isValidHttpStatus(999); // false
isValidHttpErrorStatus(404); // true - validates only error codes with classes
isValidHttpErrorStatus(200); // false - not an error status
isValidHttpErrorStatus(418); // false - not mapped in HttpErrorStatus
getHttpStatusName(404); // "NotFound"
getHttpStatusName(HttpStatus.NOT_FOUND); // "NotFound"
getHttpStatusName(200); // undefined - no error class for success codes
HttpStatus - All Standard HTTP Status Codes:
enum HttpStatus {
// 1xx Informational
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
// 2xx Success
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NO_CONTENT = 204,
// ... and more
// 3xx Redirection
MOVED_PERMANENTLY = 301,
FOUND = 302,
NOT_MODIFIED = 304,
TEMPORARY_REDIRECT = 307,
// ... and more
// 4xx Client Errors
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
// ... and more
// 5xx Server Errors
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
// ... and more
}
HttpErrorStatus - Error Codes with Mapped Error Classes:
enum HttpErrorStatus {
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
CONFLICT = 409,
PRECONDITION_FAILED = 412,
UNPROCESSABLE_ENTITY = 422,
TOO_MANY_REQUESTS = 429,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
}
All error classes extend HttpError and include:
statusCode: number - HTTP status codedata?: Record<string, any> - Additional error dataheaders?: Record<string, string | boolean | number> - Custom headerstoLambdaResponse() - Converts to API Gateway responsetoString() - Formatted string for loggingAvailable Classes:
BadRequest (400)Unauthorized (401)Forbidden (403)NotFound (404)Conflict (409)PreconditionFailed (412)UnprocessableEntity (422)TooManyRequests (429)InternalServerError (500)NotImplemented (501)BadGateway (502)ServiceUnavailable (503)Use createHttpError() to dynamically create the appropriate error based on status code:
import { createHttpError, HttpStatus } from 'awpaki';
// With enum (recommended)
const error1 = createHttpError(HttpStatus.NOT_FOUND, 'User not found', { userId: 123 });
// With number
const error2 = createHttpError(404, 'User not found', { userId: 123 });
// Creates a BadRequest error
const error3 = createHttpError(HttpStatus.BAD_REQUEST, 'Invalid input');
// Creates an Unauthorized error
const error4 = createHttpError(HttpStatus.UNAUTHORIZED, 'Token expired');
// Creates an Unauthorized error
const error3 = createHttpError(401, 'Token expired');
// Unmapped status codes fallback to NotImplemented (501)
const error4 = createHttpError(999, 'Unknown error'); // Returns NotImplemented (501)
const error5 = createHttpError(418, "I'm a teapot"); // Returns NotImplemented (501)
The HTTP_ERROR_MAP object maps status codes to error classes:
{
400: BadRequest,
401: Unauthorized,
403: Forbidden,
404: NotFound,
409: Conflict,
412: PreconditionFailed,
422: UnprocessableEntity,
429: TooManyRequests,
500: InternalServerError,
501: NotImplemented,
502: BadGateway,
503: ServiceUnavailable,
}
Usage in extractEventParams:
The extractEventParams function uses createHttpError() internally, so specifying statusCodeError in your schema will automatically throw the correct error type:
import { HttpErrorStatus } from 'awpaki';
const schema = {
pathParameters: {
id: {
label: 'User ID',
required: true,
statusCodeError: HttpErrorStatus.NOT_FOUND // Type-safe
}
},
headers: {
authorization: {
label: 'Authorization',
required: true,
statusCodeError: HttpErrorStatus.UNAUTHORIZED
}
},
body: {
email: {
label: 'Email',
required: true,
statusCodeError: HttpErrorStatus.BAD_REQUEST
}
}
};
try {
extractEventParams(schema, event);
} catch (error) {
// error will be NotFound (404), Unauthorized (401), or BadRequest (400)
// depending on which validation failed
}
Type-safe parameter types for validation:
import { ParameterType } from 'awpaki';
enum ParameterType {
STRING = 'string',
NUMBER = 'number',
BOOLEAN = 'boolean',
OBJECT = 'object',
ARRAY = 'array',
}
Usage in extractEventParams:
import { ParameterType, HttpErrorStatus } from 'awpaki';
const schema = {
pathParameters: {
id: {
label: 'User ID',
required: true,
expectedType: ParameterType.STRING, // Type-safe!
statusCodeError: HttpErrorStatus.NOT_FOUND
}
},
queryStringParameters: {
limit: {
label: 'Result Limit',
expectedType: ParameterType.NUMBER,
default: 10
},
active: {
label: 'Active Filter',
expectedType: ParameterType.BOOLEAN
}
},
body: {
tags: {
label: 'Tags',
expectedType: ParameterType.ARRAY,
required: true
}
}
};
extractEventParams<T>(schema: EventSchema, event: APIGatewayProxyEvent | Record<string, unknown>): TExtracts and validates parameters from AWS Lambda events with comprehensive validation.
Type Parameters:
T - The expected return typeParameters:
schema: EventSchema - Schema defining parameters to extract and validation rulesevent - AWS Lambda event (APIGatewayProxyEvent or custom object)Returns:
T - Extracted and validated parametersThrows:
UnprocessableEntity - When validation fails (collects multiple errors)Unauthorized - When a 401 error is configuredSchema Configuration:
Each parameter config supports:
label: string - Human-readable namerequired?: boolean - Whether requiredexpectedType?: 'string' | 'number' | 'boolean' | 'object' | 'array' - Type validationdefault?: unknown - Default value if missingstatusCodeError?: number - Custom error status codenotFoundError?: string - Custom missing messagewrongTypeMessage?: string - Custom type error messagecaseInsensitive?: boolean - Case-insensitive matchingdecoder?: (value: unknown) => unknown - Custom transformerSupported Event Sources:
pathParameters - URL path paramsqueryStringParameters - Query stringsheaders - HTTP headers (with case-insensitive support)body - Request body (auto-parsed JSON)Lambda event logging utilities for tracking and debugging in production environments.
logApiGatewayEvent(event: APIGatewayProxyEvent, context: Context, config?: LogConfig): voidLogs API Gateway events with HTTP details.
Logged Information:
Example:
import { logApiGatewayEvent } from 'awpaki';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
export const handler = async (event: APIGatewayProxyEvent, context: Context) => {
logApiGatewayEvent(event, context);
// Logs: [API Gateway Request] { httpMethod: 'GET', path: '/users/123', ... }
};
logSqsEvent(event: SQSEvent, context: Context, config?: LogConfig): voidLogs SQS events with message details.
Logged Information:
Example:
import { logSqsEvent } from 'awpaki';
import { SQSEvent, Context } from 'aws-lambda';
export const handler = async (event: SQSEvent, context: Context) => {
logSqsEvent(event, context);
// Logs: [SQS Event] { recordCount: 1, records: [...] }
};
logSnsEvent(event: SNSEvent, context: Context, config?: LogConfig): voidLogs SNS events with message details.
Logged Information:
Example:
import { logSnsEvent } from 'awpaki';
import { SNSEvent, Context } from 'aws-lambda';
export const handler = async (event: SNSEvent, context: Context) => {
logSnsEvent(event, context);
// Logs: [SNS Event] { recordCount: 1, records: [...] }
};
logEventBridgeEvent(event: EventBridgeEvent, context: Context, config?: LogConfig): voidLogs EventBridge (CloudWatch Events) including cron/scheduled events.
Logged Information:
Example:
import { logEventBridgeEvent } from 'awpaki';
import { EventBridgeEvent, Context } from 'aws-lambda';
export const handler = async (event: EventBridgeEvent<string, any>, context: Context) => {
logEventBridgeEvent(event, context);
// Logs: [EventBridge Event] { eventSource: 'aws.events', detailType: 'Scheduled Event', ... }
};
Control logging verbosity via LOG_LEVEL environment variable:
enum LogLevel {
NONE = 'none', // No logging
ERROR = 'error', // Only errors
WARN = 'warn', // Warnings and errors
INFO = 'info', // Standard info (default)
DEBUG = 'debug', // Full details (includes headers, full bodies)
}
Environment Variable:
# In your Lambda environment
LOG_LEVEL=info # Default - standard logging
LOG_LEVEL=debug # Full details (headers, complete messages)
LOG_LEVEL=none # Disable logging
Custom Configuration:
import { logApiGatewayEvent, LogLevel } from 'awpaki';
logApiGatewayEvent(event, context, {
envVar: 'CUSTOM_LOG_LEVEL', // Custom env var name
defaultLevel: LogLevel.DEBUG, // Default if env var not set
additionalData: { // Extra data to include
version: '1.0.0',
environment: 'production'
}
});
AWS SDK v3 clients with automatic retry logic using async-retry. These clients use a singleton pattern and are configured via environment variables, making them perfect for serverless applications.
dynamodbClient (Document Client)s3ClientsqsClientlambdaClientsnsClientClients are automatically configured from environment variables:
AWS_REGION or AWS_DEFAULT_REGION - AWS regionAWS_ENDPOINT_URL - Global endpoint override (useful for LocalStack)AWS_ENDPOINT_URL_DYNAMODB - DynamoDB-specific endpointAWS_ENDPOINT_URL_S3 - S3-specific endpointAWS_ENDPOINT_URL_SQS - SQS-specific endpointAWS_ENDPOINT_URL_LAMBDA - Lambda-specific endpointAWS_ENDPOINT_URL_SNS - SNS-specific endpointInstall only the AWS SDK clients you need as peer dependencies:
# For DynamoDB
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb async-retry
# For S3
npm install @aws-sdk/client-s3 async-retry
# For SQS
npm install @aws-sdk/client-sqs async-retry
# For Lambda
npm install @aws-sdk/client-lambda async-retry
# For SNS
npm install @aws-sdk/client-sns async-retry
import { dynamodbClient } from 'awpaki/clients';
import { GetCommand, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
// Simple usage - uses default retry options (3 retries)
const user = await dynamodbClient.execute(
new GetCommand({
TableName: 'Users',
Key: { id: '123' },
})
);
// With custom retry options for this specific call
const user = await dynamodbClient.execute(
new GetCommand({
TableName: 'Users',
Key: { id: '123' },
}),
{
retries: 5,
minTimeout: 500,
maxTimeout: 2000,
}
);
// Put item
await dynamodbClient.execute(
new PutCommand({
TableName: 'Users',
Item: {
id: '123',
name: 'John Doe',
email: 'john@example.com',
},
})
);
// Query with index
const results = await dynamodbClient.execute(
new QueryCommand({
TableName: 'Users',
IndexName: 'EmailIndex',
KeyConditionExpression: 'email = :email',
ExpressionAttributeValues: {
':email': 'john@example.com',
},
})
);
import { s3Client } from 'awpaki/clients';
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
// Get object
const response = await s3Client.execute(
new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'path/to/file.json',
})
);
// Read the body
const body = await response.Body.transformToString();
const data = JSON.parse(body);
// Put object
await s3Client.execute(
new PutObjectCommand({
Bucket: 'my-bucket',
Key: 'path/to/file.json',
Body: JSON.stringify({ key: 'value' }),
ContentType: 'application/json',
})
);
import { sqsClient } from 'awpaki/clients';
import {
SendMessageCommand,
ReceiveMessageCommand,
DeleteMessageCommand,
} from '@aws-sdk/client-sqs';
const queueUrl = 'https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue';
// Send message
await sqsClient.execute(
new SendMessageCommand({
QueueUrl: queueUrl,
MessageBody: JSON.stringify({ orderId: '123' }),
})
);
// Receive messages
const response = await sqsClient.execute(
new ReceiveMessageCommand({
QueueUrl: queueUrl,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20,
})
);
// Delete message
if (response.Messages) {
for (const message of response.Messages) {
await sqsClient.execute(
new DeleteMessageCommand({
QueueUrl: queueUrl,
ReceiptHandle: message.ReceiptHandle,
})
);
}
}
import { lambdaClient } from 'awpaki/clients';
import { InvokeCommand } from '@aws-sdk/client-lambda';
// Invoke function
const response = await lambdaClient.execute(
new InvokeCommand({
FunctionName: 'my-function',
Payload: JSON.stringify({
userId: '123',
action: 'processOrder',
}),
})
);
// Parse response
const result = JSON.parse(Buffer.from(response.Payload).toString());
import { snsClient } from 'awpaki/clients';
import { PublishCommand } from '@aws-sdk/client-sns';
// Publish message
await snsClient.execute(
new PublishCommand({
TopicArn: 'arn:aws:sns:us-east-1:123456789012:MyTopic',
Message: JSON.stringify({
event: 'ORDER_CREATED',
orderId: '123',
}),
MessageAttributes: {
eventType: {
DataType: 'String',
StringValue: 'ORDER_CREATED',
},
},
})
);
All clients support optional retry configuration per request:
import { dynamodbClient, type RetryOptions } from 'awpaki/clients';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
// Default retry options (used if not specified)
const defaultOptions: RetryOptions = {
retries: 3, // Number of retry attempts
minTimeout: 1000, // Initial timeout (ms)
maxTimeout: 3000, // Maximum timeout (ms)
};
// Custom retry for specific request
const user = await dynamodbClient.execute(
new GetCommand({
TableName: 'Users',
Key: { id: '123' },
}),
{
retries: 5,
minTimeout: 500,
maxTimeout: 5000,
}
);
```typescript
interface RetryOptions {
/**
* Maximum number of retries (default: 3)
*/
retries?: number;
/**
* Minimum timeout between retries in milliseconds (default: 1000)
*/
minTimeout?: number;
/**
* Maximum timeout between retries in milliseconds (default: 3000)
*/
maxTimeout?: number;
/**
* Randomize timeout (default: true)
*/
randomize?: boolean;
All clients are fully typed and work with TypeScript's type inference:
import { dynamodbClient } from 'awpaki/clients';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
// Type is inferred from the command
const response = await dynamodbClient.execute(
new GetCommand({
TableName: 'Users',
Key: { id: '123' },
})
);
// response.Item is automatically typed by AWS SDK
// Explicit typing for your data models
interface User {
id: string;
name: string;
email: string;
}
const user = response.Item as User;
const result = await db.execute<{ Item: User }>(
new GetCommand({
TableName: 'Users',
Key: { id: '123' },
})
);
// result.Item is now User type
src/
βββ index.ts # Main entry point - exports all modules
βββ clients/ # AWS SDK client abstractions
β βββ index.ts # Re-exports all clients
β βββ dynamodb/
β β βββ index.ts # DynamoDB client implementation
β β βββ index.test.ts # DynamoDB client tests
β βββ s3/
β β βββ index.ts
β β βββ index.test.ts
β βββ sqs/
β β βββ index.ts
β β βββ index.test.ts
β βββ lambda/
β β βββ index.ts
β β βββ index.test.ts
β βββ sns/
β βββ index.ts
β βββ index.test.ts
βββ parsers/ # JSON and data parsing utilities
β βββ index.ts
β βββ parseJsonBody.ts
β βββ parseJsonBody.test.ts
βββ errors/ # Custom error classes and error handling
β βββ index.ts # Re-exports all errors
β βββ http/ # HTTP error classes and status codes
β β βββ HttpError.ts
β β βββ HttpErrors.ts
β β βββ HttpErrors.test.ts
β β βββ HttpStatus.ts
β β βββ HttpStatus.test.ts
β β βββ createHttpError.test.ts
β βββ handlers/ # Lambda error handlers
β βββ handleLambdaError.ts
β βββ handleLambdaError.test.ts
βββ extractors/ # Parameter and data extraction utilities
β βββ index.ts
β βββ extractEventParams.ts
β βββ extractEventParams.test.ts
βββ loggers/ # Lambda event logging utilities
β βββ index.ts
β βββ logLambdaEvent.ts
β βββ logLambdaEvent.test.ts
βββ decoders/ # Type decoders and validators
βββ index.ts
βββ decoders.ts
βββ decoders.test.ts
For detailed guidelines on adding new features, see .github/copilot-instructions.md.
npm install
npm run build
npm test
npm run test:watch
This package is automatically deployed to npm via GitHub Actions. The deployment workflow is triggered when:
Before deploying, ensure you have:
NPM_TOKEN secret in your GitHub repository settingsv1.0.0)MIT
FAQs
AWS Patterns Kit - TypeScript utility library with AWS SDK clients, Lambda helpers, error handling, and data validation
We found that awpaki demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.