npm.io
0.4.0 • Published 2d ago

@ajentify/routed-api

Licence
MIT
Version
0.4.0
Deps
0
Size
68 kB
Vulns
0
Weekly
0

@ajentify/routed-api

A reusable CDK construct that takes a routes.json + a folder of TypeScript handler files and gives you:

  • Lambdas per route or a single-lambda router (configurable via deploymentMode)
  • DynamoDB permissions wired up declaratively
  • An HTTP API (API Gateway v2) with CORS
  • An optional Route53 + ACM custom domain
  • Everything else in your CDK stack stays exactly as you write it

No build step, no codegen, no separate deploy tool. Drop it into any CDK stack.

Why HTTP API v2?

HTTP APIs are ~70% cheaper than REST APIs, have native CORS, and are perfectly adequate for typical app backends. The construct hides API-Gateway-isms behind a single routes.json.

Install

npm install @ajentify/routed-api

Peer dependencies (your CDK app must already have these):

npm install aws-cdk-lib constructs

Minimum usage

import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as path from 'node:path';
import { RoutedApi } from '@ajentify/routed-api';

export class MyStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const todosTable = new dynamodb.Table(this, 'TodosTable', {
      partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
    });

    const api = new RoutedApi(this, 'TodoApi', {
      routesFile: path.join(__dirname, 'routes.json'),
      lambdaSrcDir: path.join(__dirname, 'lambda'),
      tables: { todos: todosTable },
      cors: { origins: ['http://localhost:3000'] },
    });

    new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
  }
}

routes.json

{
  "defaultMemoryMb": 256,
  "defaultTimeoutSeconds": 15,
  "defaultEnvironment": { "LOG_LEVEL": "info" },
  "defaultTableAccess": { "todos": "readWrite" },

  "routes": [
    {
      "path": "/todos",
      "method": "GET",
      "handler": "todos/list.ts",
      "tables": { "todos": "read" }
    },
    {
      "path": "/todos",
      "method": "POST",
      "handler": "todos/create.ts"
    },
    {
      "path": "/todos/{id}",
      "method": "DELETE",
      "handler": "todos/delete.ts"
    }
  ]
}
Route fields
Field Type Default
path string (e.g. "/todos", "/todos/{id}") required
method "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | ... | "ANY" required
handler path to .ts file, relative to lambdaSrcDir required
export exported function name in that file "handler"
memoryMb number defaultMemoryMb
timeoutSeconds number defaultTimeoutSeconds
environment Record<string,string> {}
tables string[] or Record<string, "read"|"write"|"readWrite"> inherits defaultTableAccess from props
Default table access

Pass one of:

  • "allReadWrite" — every route can read & write every table you pass in (default)
  • "allRead" — every route gets read-only on every table
  • "none" — no table access unless the route asks
  • Record<string, "read"|"write"|"readWrite"> — explicit per-table defaults

For every grant, the lambda also receives ${NAME}_TABLE_NAME as an env var (e.g. TODOS_TABLE_NAME).

Custom IAM policies

Use the policies prop to attach additional IAM permissions beyond DynamoDB table access — for example SES, S3, SQS, or any other AWS service:

import * as iam from 'aws-cdk-lib/aws-iam';

new RoutedApi(this, 'MyApi', {
  // ...
  policies: [
    new iam.PolicyStatement({
      actions: ['ses:SendEmail', 'ses:SendRawEmail'],
      resources: ['*'],
    }),
    new iam.PolicyStatement({
      actions: ['s3:GetObject'],
      resources: ['arn:aws:s3:::my-bucket/*'],
    }),
  ],
});

In single-lambda mode, policies are attached to the single router function. In lambda-per-route mode, policies are attached to every function.

Environment variables from a .env file

Instead of passing a large defaultEnvironment object inline, point to a .env file with the envFile prop:

new RoutedApi(this, 'MyApi', {
  envFile: path.join(__dirname, '.env'),
  // ...
});

The file uses standard KEY=VALUE syntax with build-time variable substitution:

# Static values
COOKIE_NAME=my_app_refresh

# Substitute from CDK build-time process.env, with fallback defaults
JWT_SECRET=${JWT_SECRET:-dev-secret-change-in-production}
API_BASE_URL=${API_BASE_URL:-http://localhost:3000}

# No fallback — empty string if unset
GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}

Variables from the env file are merged with (and overridden by) defaultEnvironment and per-route environment settings.

CORS
cors: {
  origins: ['http://localhost:3000', 'https://app.example.com'],
  // methods, headers, allowCredentials, maxAgeSeconds are all optional
}
// or pass false to disable preflight entirely

Sensible defaults are applied — Content-Type, Authorization, X-Api-Key, X-Amz-Date, X-Amz-Security-Token, X-Requested-With.

Custom domain (Route53 + ACM)
domain: {
  recordName: 'api.example.com',
  hostedZoneName: 'example.com',
  certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abcd',
  // hostedZoneId optional — if omitted the zone is looked up by name
}

ACM cert must be in the same region as the API (HTTP API uses regional endpoints, not edge-optimized). If you need an edge cert, you'd typically front the API with CloudFront instead.

Handlers

Each handler is a normal APIGatewayProxyHandlerV2. The construct uses CDK's NodejsFunction, so esbuild compiles your TypeScript automatically — no build step needed.

// lambda/todos/list.ts
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export const handler: APIGatewayProxyHandlerV2 = async () => {
  const out = await ddb.send(new ScanCommand({ TableName: process.env.TODOS_TABLE_NAME! }));
  return {
    statusCode: 200,
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ items: out.Items ?? [] }),
  };
};

Your CDK app should have @aws-sdk/* packages and aws-lambda types installed:

npm install --save-dev @types/aws-lambda
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

(@aws-sdk/* is external in the bundle — Lambda's Node 20 runtime already includes it.)

What about non-API stuff?

Just keep writing CDK normally. RoutedApi is one block in your stack — add SES identities, processing lambdas, EventBridge rules, S3 buckets, whatever, before or after it.

new RoutedApi(this, 'Api', { ... });

new lambda.Function(this, 'BackgroundProcessor', { ... });  // totally fine
new ses.EmailIdentity(this, 'EmailIdentity', { ... });      // also fine

Worked example

See example/ in this folder:

  • example/routes.json — the routes file
  • example/lambda/todos/{list,create,delete}.ts — handler files
  • example/example-stack.ts — the CDK stack

Keywords