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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lafken/api

v0.14.2

Published

Define AWS REST APIs using TypeScript decorators - declarative, type-safe infrastructure for API Gateway

Readme

@lafken/api

Build AWS REST APIs using TypeScript decorators. @lafken/api lets you declare endpoints, request/response models, authorizers, and AWS service integrations directly in your classes — Lafken generates all the API Gateway and Lambda infrastructure for you.

Installation

npm install @lafken/api

Getting Started

Register the ApiResolver in your application and define your first API resource:

import { createApp, createModule } from '@lafken/main';
import { ApiResolver } from '@lafken/api/resolver';
import { Api, Get, Post, Event, ApiRequest, BodyParam } from '@lafken/api/main';

// 1. Define request payload
@ApiRequest()
class CreateTaskPayload {
  @BodyParam({ minLength: 1 })
  title: string;
}

// 2. Define the API resource
@Api({ path: '/tasks' })
class TaskApi {
  @Get()
  list() {
    return [{ id: 1, title: 'Review PR' }];
  }

  @Post()
  create(@Event(CreateTaskPayload) payload: CreateTaskPayload) {
    return { id: 2, title: payload.title };
  }
}

// 3. Register it in a module
const taskModule = createModule({
  name: 'tasks',
  resources: [TaskApi],
});

// 4. Add the resolver to your app
createApp({
  name: 'my-app',
  resolvers: [
    new ApiResolver({
      restApi: {
        name: 'my-rest-api',
        cors: { allowOrigins: true },
        stage: { stageName: 'dev' },
      },
    }),
  ],
  modules: [taskModule],
});

If no configuration is passed to ApiResolver, a default API Gateway is created with minimal settings. You can also create multiple APIs within the same application by passing multiple configuration objects.

API Definition: resource vs openapi

The definition option on restApi controls how API Gateway generates and manages your API structure. It accepts two values:

| Value | Default | Description | | ---------- | ------- | ----------------------------------------------------------------------------------------------------- | | resource | ✅ | Each element of the API (resources, methods, integrations, models, validators, authorizers, docs) is created as an individual Terraform resource. | | openapi | | The whole API structure is assembled into a single OpenAPI 3.0 document (with x-amazon-apigateway-* extensions) and imported into the REST API body. |

new ApiResolver({
  restApi: {
    name: 'my-app',
    definition: 'openapi',
  },
});

resource mode (default)

Each API Gateway element becomes its own Terraform resource (aws_api_gateway_resource, aws_api_gateway_method, aws_api_gateway_integration, aws_api_gateway_model, aws_api_gateway_request_validator, aws_api_gateway_authorizer, documentation parts, and so on).

  • Pros: granular and debuggable — Terraform reports exactly which resource changed, and each element can be inspected independently in the AWS console.
  • Cons: on large APIs, deployment is slower because Terraform diffs and verifies every resource on each deploy.

openapi mode

All paths, methods, integrations, schemas, request validators, authorizers, gateway responses, and documentation parts are accumulated into a single OpenAPI 3.0 document and injected into the REST API body. API Gateway imports the whole API in one shot.

  • Pros: significantly faster deploys on large APIs — a single body update replaces dozens of individual resource diffs.
  • Cons: less granular — the API structure is managed as one blob, so Terraform diffs it as a whole.

Side resources that the body only references are still created as regular Terraform resources in both modes: Lambda functions and permissions, IAM roles, usage plans and API keys, gateway responses, log groups, deployments, and stages.

Documentation in the exported spec

Both modes support the same decorator-driven documentation — tags and description on @Api, and summary, description, and tags on method decorators:

@Api({
  path: '/articles',
  tags: ['Articles'],
})
export class ArticleApi {
  @Get({
    path: '/{id}',
    summary: 'Get article by id',
    description: 'Get the full article details',
  })
  getById() { /* ... */ }
}

In openapi mode this documentation is emitted inline in the OpenAPI document (tags, summary, description fields) and included in any OAS3 export from the deployed API — whether generated by aws apigateway get-export or the AWS console. To make the export include the full documentation, the stage references a documentation version that Lafken creates automatically.

In resource mode the same information is stored as aws_api_gateway_documentation_part resources and an associated documentation version.

Supported integrations

All AWS service integrations work in both modes (bucket, dynamodb, queue, state-machine, event-bridge, and plain Lambda-backed methods). If an integration is not supported in openapi mode, deployment fails with a clear error.

Features

HTTP Methods

Use @Get, @Post, @Put, @Patch, @Delete, @Head, and @Any to define endpoints. Each decorator creates a Lambda-backed method on the API Gateway resource defined by @Api.

The method path is appended to the base path set in @Api:

import { Api, Get, Post, Put, Delete } from '@lafken/api/main';

@Api({ path: '/articles' })
class ArticleApi {
  @Get()
  listAll() {
    return [{ id: 1, title: 'Getting Started' }];
  }

  @Get({ path: '{id}' })
  getById() {
    return { id: 1, title: 'Getting Started' };
  }

  @Post()
  create() {
    return { id: 2, title: 'New Article' };
  }

  @Put({ path: '{id}' })
  update() {
    return { updated: true };
  }

  @Delete({ path: '{id}' })
  remove() {
    return { deleted: true };
  }
}

Request Events

Handler methods receive structured input through the @Event decorator combined with an @ApiRequest class. Field decorators specify where each value is extracted from in the HTTP request:

| Decorator | Source | Always Required | | ---------------- | ------------------------------ | --------------- | | @BodyParam | Request body | Yes (default) | | @PathParam | URL path parameter | Yes (always) | | @QueryParam | Query string parameter | Yes (default) | | @HeaderParam | HTTP header | Yes (default) | | @ContextParam | API Gateway request context | Yes (always) |

These decorators generate a fully resolved Velocity requestTemplate internally, mapping each field to the correct source.

import { Api, Post, Event, ApiRequest, PathParam, BodyParam, QueryParam } from '@lafken/api/main';

@ApiRequest()
class CreateCommentPayload {
  @PathParam()
  articleId: number;

  @BodyParam({ minLength: 1, maxLength: 500 })
  content: string;

  @QueryParam({ required: false })
  notify: string;
}

@Api({ path: '/articles' })
class ArticleApi {
  @Post({ path: '{articleId}/comments' })
  addComment(@Event(CreateCommentPayload) payload: CreateCommentPayload) {
    return { articleId: payload.articleId, content: payload.content };
  }
}

Body Parameter Validation

@BodyParam supports type-specific validation constraints that map to OpenAPI schema attributes:

@ApiRequest()
class SignupPayload {
  @BodyParam({ minLength: 3, maxLength: 50 })
  username: string;

  @BodyParam({ format: 'email' })
  email: string;

  @BodyParam({ min: 18, max: 120 })
  age: number;

  @BodyParam({ minItems: 1, uniqueItems: true })
  roles: string[];
}

Nested Request Objects

Use @RequestObject (an alias for @ApiRequest) to define nested structures within a request payload:

import { ApiRequest, RequestObject, BodyParam } from '@lafken/api/main';

@RequestObject()
class Address {
  @BodyParam()
  street: string;

  @BodyParam()
  city: string;
}

@ApiRequest()
class CreateContactPayload {
  @BodyParam({ minLength: 1 })
  name: string;

  @BodyParam({ type: Address })
  address: Address;
}

Context Parameters

Access API Gateway context variables such as request IDs or client IPs using @ContextParam:

@ApiRequest()
class AuditedPayload {
  @BodyParam()
  action: string;

  @ContextParam({ name: 'requestId' })
  requestId: string;

  @ContextParam({ name: 'identity.sourceIp' })
  clientIp: string;
}

AWS Service Integrations

HTTP methods can integrate directly with AWS services without an intermediate Lambda function. Set the integration property on the method decorator and use @IntegrationOptions to reference other infrastructure resources via getResourceValue.

Supported integrations:

| Integration | Actions | | ---------------- | ------------------------------ | | bucket | Download, Upload, Delete | | dynamodb | Query, Put, Delete | | queue | SendMessage | | state-machine | Start, Stop, Status | | event-bridge | PutEvents |

S3 Bucket Integration

import {
  Api,
  Get,
  Put,
  IntegrationOptions,
  type BucketIntegrationOption,
  type BucketIntegrationResponse,
} from '@lafken/api/main';

@Api({ path: '/documents' })
class DocumentApi {
  @Get({
    integration: 'bucket',
    action: 'Download',
  })
  download(
    @IntegrationOptions() { getResourceValue }: BucketIntegrationOption,
  ): BucketIntegrationResponse {
    return {
      bucket: getResourceValue('bucket::project-documents', 'id'),
      object: 'report.pdf',
    };
  }

  @Put({
    integration: 'bucket',
    action: 'Upload',
  })
  upload(
    @IntegrationOptions() { getResourceValue }: BucketIntegrationOption,
  ): BucketIntegrationResponse {
    return {
      bucket: getResourceValue('bucket::project-documents', 'id'),
      object: 'new-report.pdf',
    };
  }
}

DynamoDB Integration

import {
  Api,
  Get,
  Post,
  IntegrationOptions,
  type DynamoIntegrationOption,
  type DynamoQueryIntegrationResponse,
  type DynamoPutIntegrationResponse,
} from '@lafken/api/main';

@Api({ path: '/products' })
class ProductApi {
  @Get({
    integration: 'dynamodb',
    action: 'Query',
  })
  search(
    @IntegrationOptions() { getResourceValue }: DynamoIntegrationOption,
  ): DynamoQueryIntegrationResponse {
    return {
      tableName: getResourceValue('dynamo::products-table', 'id'),
      partitionKey: { category: 'electronics' },
    };
  }

  @Post({
    integration: 'dynamodb',
    action: 'Put',
  })
  add(
    @IntegrationOptions() { getResourceValue }: DynamoIntegrationOption,
  ): DynamoPutIntegrationResponse {
    return {
      tableName: getResourceValue('dynamo::products-table', 'id'),
      data: { name: 'Keyboard', price: 75 },
    };
  }
}

SQS Queue Integration

import {
  Api,
  Post,
  Event,
  IntegrationOptions,
  type QueueIntegrationOption,
  type QueueSendMessageIntegrationResponse,
} from '@lafken/api/main';

@Api({ path: '/notifications' })
class NotificationApi {
  @Post({
    integration: 'queue',
    action: 'SendMessage',
  })
  enqueue(
    @IntegrationOptions() { getResourceValue }: QueueIntegrationOption,
  ): QueueSendMessageIntegrationResponse {
    return {
      queueName: getResourceValue('queue::notification-queue', 'id'),
      body: { type: 'welcome', recipient: 'new-user' },
    };
  }
}

The body accepts a plain string, the whole @Event object or an object literal that mixes static values and event fields, including nested ones:

@Post({
  path: '/{userId}',
  integration: 'queue',
  action: 'SendMessage',
})
enqueueUser(@Event(NotificationPayload) e: NotificationPayload): QueueSendMessageIntegrationResponse {
  return {
    queueName: 'notification-queue',
    body: {
      type: 'welcome',
      userId: e.userId,
      email: e.contact.email,
      channels: e.channels,
    },
  };
}

State Machine Integration

import {
  Api,
  Post,
  Get,
  IntegrationOptions,
  type StateMachineIntegrationOption,
  type StateMachineStartIntegrationResponse,
  type StateMachineStatusIntegrationResponse,
} from '@lafken/api/main';

@Api({ path: '/workflows' })
class WorkflowApi {
  @Post({
    integration: 'state-machine',
    action: 'Start',
  })
  start(
    @IntegrationOptions() { getResourceValue }: StateMachineIntegrationOption,
  ): StateMachineStartIntegrationResponse {
    return {
      stateMachineArn: getResourceValue('state-machine::processing-workflow', 'arn'),
      input: { step: 'begin' },
    };
  }

  @Get({
    integration: 'state-machine',
    action: 'Status',
  })
  status(
    @IntegrationOptions() { getResourceValue }: StateMachineIntegrationOption,
  ): StateMachineStatusIntegrationResponse {
    return {
      executionArn: getResourceValue('state-machine::processing-workflow', 'arn'),
    };
  }
}

EventBridge Integration

import {
  Api,
  Post,
  IntegrationOptions,
  type EventBridgeIntegrationOption,
  type EventBridgePutEventsIntegrationResponse,
} from '@lafken/api/main';

@Api({ path: '/events' })
class EventApi {
  @Post({
    integration: 'event-bridge',
    action: 'PutEvents',
  })
  publish(
    @IntegrationOptions() { getResourceValue }: EventBridgeIntegrationOption,
  ): EventBridgePutEventsIntegrationResponse {
    return {
      eventBusName: getResourceValue('event-bus::orders-bus', 'id'),
      source: 'orders',
      detailType: 'OrderCreated',
      detail: { orderId: '123' },
    };
  }
}

The detail payload is serialized into the EventBridge Detail JSON string and can mix static values with @Event fields, including nested ones:

@Post({
  path: '/{orderId}',
  integration: 'event-bridge',
  action: 'PutEvents',
})
publishOrder(
  @Event(OrderPayload) e: OrderPayload,
): EventBridgePutEventsIntegrationResponse {
  return {
    eventBusName: 'orders-bus',
    source: 'orders',
    detailType: 'OrderCreated',
    detail: {
      orderId: e.orderId,
      customer: e.customer.email,
    },
  };
}

When eventBusName is omitted, the event is published to the default event bus.

Responses

You can return values directly from handler methods without defining a response type. However, for more control over status codes and response models, use the @ApiResponse and @ResField decorators.

Basic Response Model

Define a response class and pass it to the method decorator via the response property:

import { Api, Get, ApiResponse, ResField } from '@lafken/api/main';

@ApiResponse()
class ArticleResponse {
  @ResField()
  title: string;

  @ResField()
  views: number;
}

@Api({ path: '/articles' })
class ArticleApi {
  @Get({ path: '{id}', response: ArticleResponse })
  getById(): ArticleResponse {
    return { title: 'Getting Started', views: 42 };
  }
}

Multiple Status Codes

Map different HTTP status codes to distinct response classes. Use true for responses without a body:

import { Api, Post, ApiResponse, ResField, response } from '@lafken/api/main';

@ApiResponse()
class ErrorResponse {
  @ResField()
  message: string;
}

@ApiResponse({
  responses: {
    400: ErrorResponse,
    204: true,
  },
})
class CreateArticleResponse {
  @ResField()
  id: number;

  @ResField()
  title: string;
}

@Api({ path: '/articles' })
class ArticleApi {
  @Post({ response: CreateArticleResponse })
  create(): CreateArticleResponse {
    const isInvalid = false;

    if (isInvalid) {
      response<ErrorResponse>(400, { message: 'Title is required' });
    }

    return { id: 1, title: 'New Article' };
  }
}

The response() function returns a response with a specific status code that API Gateway interprets correctly.

Default Status Codes

If no response property is set, default status codes are generated automatically (20X for success, 400 and 500 for errors). The default success code depends on the HTTP method:

  • POST defaults to 201
  • All other methods default to 200

Override the default code with defaultCode:

@ApiResponse({
  defaultCode: 202,
})
class AsyncResponse {
  @ResField()
  jobId: string;
}

Nested Response Objects

Use @ResponseObject to define nested structures within a response:

import { ApiResponse, ResponseObject, ResField } from '@lafken/api/main';

@ResponseObject()
class AuthorInfo {
  @ResField()
  name: string;

  @ResField()
  email: string;
}

@ApiResponse()
class ArticleDetailResponse {
  @ResField()
  title: string;

  @ResField({ type: AuthorInfo })
  author: AuthorInfo;
}

Authorizers

Lafken supports three authorization strategies: API Key, Custom Lambda, and Cognito. Each is defined as a decorated class and registered in the ApiResolver.

API Key Authorizer

Protects endpoints by requiring a valid API key. Optionally configure quota limits and throttling:

import { ApiKeyAuthorizer } from '@lafken/api/main';

@ApiKeyAuthorizer({
  name: 'platform-api-key',
  defaultKeys: ['default-key'],
  quota: { limit: 10000, period: 'month' },
  throttle: { burstLimit: 50, rateLimit: 100 },
})
export class PlatformApiKey {}

Custom Authorizer

Implement your own authentication logic with a Lambda-backed authorizer. The class must include a method decorated with @AuthorizerHandler:

import {
  CustomAuthorizer,
  AuthorizerHandler,
  type AuthorizationHandlerEvent,
  type AuthorizerResponse,
} from '@lafken/api/main';

@CustomAuthorizer({
  name: 'token-auth',
  header: 'Authorization',
  authorizerResultTtlInSeconds: 300,
})
export class TokenAuthorizer {
  @AuthorizerHandler()
  validate(event: AuthorizationHandlerEvent): AuthorizerResponse {
    const isValid = event.headers?.Authorization === 'Bearer valid-token';

    return {
      principalId: '[email protected]',
      allow: isValid,
    };
  }
}

The handler receives an AuthorizationHandlerEvent — the standard APIGatewayRequestAuthorizerEvent enriched with a permissions array containing the scopes configured for the invoked method. It must return an AuthorizerResponse with allow and principalId.

Cognito Authorizer

Integrates with an Amazon Cognito User Pool for token-based authorization. Requires @lafken/auth to be configured first:

import { CognitoAuthorizer } from '@lafken/api/main';

@CognitoAuthorizer({
  userPool: 'main-user-pool',
  name: 'cognito-auth',
  header: 'Authorization',
  authorizerResultTtlInSeconds: 300,
})
export class MainCognitoAuth {}

Registering Authorizers

Pass authorizer classes to the ApiResolver and optionally set a default authorizer for all methods:

new ApiResolver({
  restApi: {
    name: 'my-rest-api',
    auth: {
      authorizers: [PlatformApiKey, TokenAuthorizer],
      defaultAuthorizerName: 'token-auth',
    },
  },
});

Applying Authorizers

The auth property is available on both @Api (class-level) and method decorators (@Get, @Post, etc.). Method-level settings override class-level ones.

Apply to all methods in a class:

@Api({
  path: '/admin',
  auth: { authorizerName: 'platform-api-key' },
})
class AdminApi { /* ... */ }

Apply to a specific method:

@Get({
  path: '{id}',
  auth: { authorizerName: 'token-auth' },
})
getById() { /* ... */ }

Disable authorization for a specific method or entire class:

@Get({ auth: false })
healthCheck() {
  return { status: 'ok' };
}

Scopes

Both Custom and Cognito authorizers support scopes — an array of strings delivered to the authorizer handler via the permissions property:

@Delete({
  path: '{id}',
  auth: {
    authorizerName: 'token-auth',
    scopes: ['article:delete'],
  },
})
remove() { /* ... */ }

Extending the API

The ApiResolver accepts an extend function that receives the generated API instance and the app scope. Use it to apply advanced CDKTN configuration such as custom domains or additional settings:

new ApiResolver({
  restApi: {
    name: 'my-rest-api',
  },
  extend: ({ api, scope }) => {
    // Add a custom domain, WAF, or any CDKTN construct
  },
});