npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

povery

v0.0.45

Published

Povery - a Lambda Framework

Readme

Povery

A TypeScript Framework for AWS Lambda

workflow

Povery is a lightweight, decorator-based framework designed specifically for building serverless applications on AWS Lambda with TypeScript. It provides a structured approach to handling API Gateway requests, AWS events, and authentication/authorization flows.

Check out povery-cli for organizing applications built with Povery.

Table of Contents

Why Povery?

AWS Lambda operates fundamentally differently from traditional web servers. While many developers attempt to use Express.js with Lambda, this approach introduces several significant issues.

Povery addresses these challenges by:

  • Providing a Lambda-native approach to handling HTTP requests
  • Introducing familiar concepts like controllers, decorators, and middleware
  • Offering built-in support for parameter validation and type safety
  • Integrating seamlessly with AWS services like API Gateway and Cognito

Installation

npm i povery

Usage

API Gateway Integration

Handle API Gateway proxy requests with clean, decorator-based controllers:

import { povery, controller, api } from 'povery'

@controller
class Controller {
    @api('GET', '/hello')
    sayHello() {
        return "We're povery!"
    }
}

exports.handler = povery.load(Controller);

This code responds to GET requests on the /hello route and automatically formats the response for API Gateway:

{
    "headers": {},
    "isBase64Encoded": false,
    "statusCode": 200,
    "body": "We're povery!" 
}

AWS Event Handling

Process non-HTTP AWS events with the forAwsEvent middleware:

import { povery, forAwsEvent } from "povery";

async function handler(event, context) {
  // Process the AWS event
}

exports.handler = povery
    .use(forAwsEvent())
    .load(handler);

The forAwsEvent middleware marks the context as an AWS event, which changes how Povery processes the request. Instead of trying to match HTTP routes, it directly passes the event to your handler function. This is useful for handling AWS events like:

  • S3 bucket events
  • DynamoDB stream events
  • CloudWatch scheduled events
  • SNS notifications
  • SQS message processing
  • Custom events from other Lambda functions

RPC Functionality

In addition to HTTP routing, Povery supports RPC-style (Remote Procedure Call) invocations of controller methods:

import { povery } from "povery";

@controller
class RPCController {
    calculateSum(payload) {
        const { a, b } = payload;
        return { result: a + b };
    }
    
    getUserData(payload) {
        const { userId } = payload;
        // Fetch and return user data
        return { user: { id: userId, name: "Example    User" } };
    }
}

exports.handler = povery.load(RPCController);

To invoke these methods, send a request with action and payload properties:

{
    "action": "calculateSum",
    "payload": {
        "a": 5,
        "b": 3
    }
}

The response will be the direct return value from the method:

{
    "result": 8
}

RPC calls are useful when:

  • You need direct method invocation without HTTP routing overhead
  • You're building internal services that communicate between Lambda functions
  • You want a simpler interface for function-to-function communication

Middleware System

Povery uses a middleware system to intercept and process events before and after they reach your handler functions. Middlewares are stateless, aligning with Lambda's execution model:

const exampleMiddleware = {
    setup: (event, context) => {
        // Executed BEFORE the handler method
        // event and context are passed by reference and can be modified
    }, 
    teardown: (event, context, executionResult, error) => {
        // Executed AFTER the handler method
        // Can modify the final response
    }
}

exports.handler = povery
    .use(exampleMiddleware)
    .load(Controller);

Middlewares can be async and are executed in the order they're added.

Authentication & Authorization

Povery doesn't handle authentication directly, as this is best delegated to API Gateway and AWS Cognito. However, it provides tools for authorization:

import { povery, controller, api, Authorizer, acl } from 'povery';

@controller
class Controller {
    // Only users with ADMIN role can access
    @api('GET', '/admin-only')
    @acl(['ADMIN'])
    adminOnlyEndpoint() {
        const user = Auth.getUser();
        const roles = Auth.getRoles();
        return 'Admin access granted';
    }
}

exports.handler = povery
    .use(Authorizer(Controller))
    .load(Controller);

By default, povery reads roles from Cognito Groups. If your user role is set on a user pool attribute, you can use the Authorizer middleware like this:

exports.handler = povery
  .use(
    Authorizer(Controller, {
      roleClaim: "custom:role"
    })
  )
  .load(Controller);

API Stage Handling

When using AWS HTTP APIs, the stage name is included in the path. Povery automatically handles this by removing the stage prefix from the path before matching routes:

// Example:
// API Stage: dev
// Original path: /dev/users
// Path used for route matching: /users

This ensures that your route definitions don't need to include the stage name. The stage removal is only applied when:

  1. The request includes a stage name in the requestContext.stage property
  2. The path starts with the stage name followed by a slash (/{stage}/...)

This prevents issues with paths that might contain the stage name as part of a resource path (e.g., /dev/devices when the stage is dev).

Request Parameters

Povery provides several decorators to handle and validate request parameters:

Path Parameters

Path parameters are automatically validated and transformed:

@api('GET', '/user/:id')
async getUserById(
    event: any,
    context: any,
    @pathParam({name: 'id', validators: [IsUUID('4')]}) id: string
) {
    // id contains the validated UUID from the path
    return getUserService.findById(id);
}

You can also transform path parameters to different types using the transform option:

@api('GET', '/products/:id')
async getProduct(
    event: any,
    context: any,
    @pathParam({
        name: 'id',
        transform: (val) => parseInt(val, 10)
    }) id: number
) {
    // id is now a number, not a string
    return productService.findById(id);
}

The transform function is applied to the specific parameter value, allowing you to convert strings from the URL path to appropriate types like numbers, booleans, or custom objects.

Request Body

The @body decorator is used to validate and transform the request body. It supports automatic type conversion and validation:

@api('PATCH', '/user/:id')
async updateUser(
    event: any,
    context: any,
    @body({
        transform: (event: any) => UserDto.fromObject(event), 
        validate: true
    }) userDto: UserDto
) {
    // userDto contains the validated request body
    return userService.update(userDto);
}

Query Parameters

Individual query parameters:

@api('GET', '/users')
async getUsers(
    event: any,
    context: any,
    @queryParam({name: 'status', validators: [IsString()]}) status: string
) {
    // EXAMPLE CODE, status contains the validated query parameter
    return userService.findByStatus(status);
}

Multiple query parameters:

@api('GET', '/users')
async getUsers(
    event: any,
    context: any,
    @queryParams({
        transform: (params) => new UserFilterDto(params),
        validate: true
    }) filters: UserFilterDto
) {
    // filters contains all validated query parameters
    return userService.findWithFilters(filters);
}

Custom Parameters

Create custom parameter decorators:

@api('GET', '/timestamp')
async getWithTimestamp(
    event: any,
    context: any,
    @autowiredParam(() => new Date().toISOString()) timestamp: string
) {
    console.log(timestamp); // Current ISO timestamp
    return { timestamp };
}

Validation

Povery uses class-validator for parameter validation, providing type safety and runtime validation.

Validation with Parameter Decorators

Each parameter decorator (@body, @pathParam, @queryParam, etc.) supports validation through the validators option:

import { IsUUID, IsString, IsInt, Min, Max } from 'class-validator';

@api('GET', '/products/:id')
async getProduct(
    event: any,
    context: any,
    @pathParam({name: 'id', validators: [IsUUID('4')]}) id: string,
    @queryParam({name: 'fields', validators: [IsString()]}) fields: string,
    @queryParam({name: 'limit', validators: [IsInt(), Min(1), Max(100)]}) limit: number
) {
    // All parameters are validated before this code runs
    return productService.findById(id, fields, limit);
}

DTO Validation

For complex objects like request bodies, create Data Transfer Objects (DTOs) with validation rules:

import { IsEmail, IsString, Length, IsOptional, IsBoolean } from 'class-validator';

export class CreateUserDto {
    @IsString()
    @Length(2, 50)
    name: string;
    
    @IsEmail()
    email: string;
    
    @IsString()
    @Length(8, 100)
    password: string;
    
    @IsOptional()
    @IsBoolean()
    isActive?: boolean;
    
    static fromObject(obj: any): CreateUserDto {
        const dto = new CreateUserDto();
        Object.assign(dto, obj);
        return dto;
    }
}

Then use it with the @body decorator:

@api('POST', '/users')
async createUser(
    event: any,
    context: any,
    @body({
        transform: (event: any) => CreateUserDto.fromObject(JSON.parse(event.body)), 
        validate: true
    }) userDto: CreateUserDto
) {
    // userDto is fully validated and typed
    return userService.create(userDto);
}

Validation Error Handling

When validation fails, Povery automatically returns an error response with status code 400 and details about the validation errors:

{
    "statusCode": 400,
    "body": {
        "errorMessage": "Validation failed",
        "errorData": [
            {
                "property": "email",
                "constraints": {
                    "isEmail": "email must be a valid email address"
                }
            },
            {
                "property": "password",
                "constraints": {
                    "length": "password must be longer than or equal to 8 characters"
                }
            }
        ]
    }
}

Custom Validation

You can create custom validators by implementing the ValidatorConstraintInterface:

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

@ValidatorConstraint({ name: 'isStrongPassword', async: false })
export class IsStrongPasswordConstraint implements ValidatorConstraintInterface {
    validate(password: string, args: ValidationArguments) {
        // Password must contain at least one uppercase letter, one lowercase letter,
        // one number, and one special character
        const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
        return regex.test(password);
    }
    
    defaultMessage(args: ValidationArguments) {
        return 'Password is not strong enough';
    }
}

Then use it in your DTOs:

import { Validate } from 'class-validator';

export class UserDto {
    // Other properties...
    
    @Validate(IsStrongPasswordConstraint)
    password: string;
}

Validation Best Practices

  1. Always validate user input: Never trust client-side data
  2. Use specific validators: Choose the most specific validator for each field
  3. Provide meaningful error messages: Customize error messages to help users fix issues
  4. Combine validators: Use multiple validators to enforce complex rules
  5. Create reusable DTOs: Define DTOs that can be reused across endpoints
  6. Separate validation from business logic: Keep validation in DTOs and decorators

By leveraging Povery's validation system, you can ensure that your Lambda functions only process valid data, reducing bugs and improving security.

Examples

The examples directory contains several sample projects demonstrating how to use Povery in different scenarios:

Basic Setup

A simple example showing how to set up a basic API with Povery.

import { povery, controller, api } from 'povery'

@controller
class Controller {
    @api('GET', '/test')
    sayHello() {
        return `We're povery!`
    }
}

exports.handler = povery.load(Controller);

This example demonstrates:

  • Basic controller setup
  • API endpoint definition
  • Lambda handler export

Authentication with Cognito

Demonstrates how to implement authentication and authorization using AWS Cognito.

import { povery, controller, api, acl, Authorizer, Auth } from 'povery';

@controller
class AuthController {
    @api('GET', '/profile')
    @acl(['USER', 'ADMIN'])
    getUserProfile() {
        const user = Auth.getUser();
        const roles = Auth.getRoles();
        
        return {
            message: 'You are authenticated!',
            user: {
                username: user.username,
                email: user.email
            },
            roles: roles
        };
    }
    
    @api('GET', '/admin')
    @acl(['ADMIN'])
    getAdminDashboard() {
        return {
            message: 'Welcome to the admin dashboard!',
            secretData: 'This is only visible to admins'
        };
    }
}

exports.handler = povery
    .use(Authorizer(AuthController))
    .load(AuthController);

This example demonstrates:

  • AWS Cognito integration
  • Role-based access control
  • Protected endpoints

Database Integration

Shows how to implement database connections following AWS Lambda best practices.

import { povery, controller, api, body, pathParam } from 'povery';
import { executeQuery } from './db';
import { User, CreateUserDto } from './models';

@controller
class UserController {
    @api('GET', '/users')
    async getUsers() {
        try {
            const users = await executeQuery<User>('SELECT * FROM users LIMIT 100');
            return { users };
        } catch (error) {
            console.error('Error fetching users:', error);
            throw new PoveryError('Failed to fetch users', 500);
        }
    }
    
    @api('POST', '/users')
    async createUser(
        event: any,
        context: any,
        @body({
            transform: (event: any) => CreateUserDto.fromObject(JSON.parse(event.body)),
            validate: true
        }) userDto: CreateUserDto
    ) {
        // Implementation...
    }
}

exports.handler = povery.load(UserController);

This example demonstrates:

  • PostgreSQL connection pooling and reuse
  • Secure credential management with AWS Secrets Manager
  • Parameter validation with DTOs
  • Error handling

Lambda Layers

Demonstrates how to use AWS Lambda Layers for sharing code across multiple functions.

// In Lambda function:
import { povery, controller, api, pathParam } from 'povery';
import { productService, GetProductDto } from '/opt/nodejs';

@controller
class ProductController {
    @api('GET', '/products')
    async getProducts() {
        const products = await productService.getAllProducts();
        return { products };
    }
    
    @api('GET', '/products/:id')
    async getProductById(
        event: any,
        context: any,
        @pathParam({ name: 'id', validators: [] }) id: string
    ) {
        // Implementation...
    }
}

exports.handler = povery.load(ProductController);

This example demonstrates:

  • Shared models and services in a Lambda Layer
  • PostgreSQL database service in a shared layer
  • Code reuse across functions
  • Reduced bundle size

For more detailed examples, check out the examples directory in the repository.

Logging

Set the environment variable LOG_LEVEL to DEBUG for detailed execution information. Be cautious with sensitive data and CloudWatch costs.

TypeScript Configuration

Enable decorator support in your tsconfig.json:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Testing

Povery provides a withContext helper for testing:

import { withContext } from 'povery';

describe('User Controller', () => {
    it('should get user by ID', withContext(
        {
            user: {
                "email": "[email protected]"
            }
        },
        async () => {
            // Test code here
            const result = await userController.getUserById('123');
            expect(result).toBeDefined();
        }
    ));
});

Contributing

Feel free to open issues and pull requests.

License

MIT