effect-lambda

effect-lambda

NPM Version npm npm GitHub Pages

Effect friendly wrapper for AWS Lambda functions.

Disclaimer: This library is still in early development stage and the API is likely to change. Feedback is welcome.

Have been using lambda functions as a primary way to build serverless applications for a while now. Since I made the switch from fp-ts to effect, I wanted to use effects all the way when writing lambda functions, replacing the previous usage of middy and fp-ts. This library is an attempt to provide a functional way to write lambda functions using the effect library. The library is inspired by the @effect/platform library and aims to provide a similar experience for writing lambda functions.

Pros: The main approach of this library to use simple Effects as lambda handlers, allowing access at any point to the event and context allowing some really cool patterns.

  • For some of the handlers, this lends itself to more accessible patterns, like an API Gateway handler that provides the payload base64 encoded and stringified in the event body, so being able to abstract away that or normalize headers is quite useful.
  • Or take an SQS handler which can operate on individual records in a batch, and provide a utility to handle batch failures.

Take a look at the following example:

import {
APIGatewayProxyEvent,
schemaBodyJson,
toLambdaHandler,
} from "effect-lambda/RestApi";
import { Effect, Console } from "effect";
import { Schema } from "@effect/schema";

const PayloadSchema = Schema.Struct({
message: Schema.String,
});

export const _handler = schemaBodyJson(PayloadSchema).pipe(
Effect.map((payload) => ({
statusCode: 200,
body: JSON.stringify({ message: payload.message }),
})),
Effect.catchTag("ParseError", () =>
Effect.succeed({
statusCode: 400,
body: "Bad Request",
}),
),
);

export const handler = _handler.pipe(toLambdaHandler);

// Or you can add a post processing middleware to the handler just by mapping over the effect
export const handlerWithMiddleware = _handler.pipe(
Effect.map((response) => ({
...response,
headers: { "Content-Type": "application/json" },
})),
toLambdaHandler,
);

// Or you can add a pre-processing middleware
export const handlerWithPreMiddleware = APIGatewayProxyEvent.pipe(
Effect.tap((event) => Console.log(`Received event: ${event}`)),
Effect.flatMap(() => _handler),
toLambdaHandler,
);

Cons:

  • This approach of making the event accessible at any point in the handler requires individual wrappers for each type of event.
  • When using a layered architecture, the lambda specific wrapper should be fairly thin, essentially just extracting the domain input for the "use case" layer and map back the domain output to the lambda output.

This library has peer dependencies on @effect/schema and effect. You can install them via npm or pnpm or any other package manager you prefer.

# pnpm
pnpm add effect-lambda effect @effect/schema
# npm
npm install effect-lambda effect @effect/schema

Currently the library provides handlers for the following AWS Lambda triggers:

  • API Gateway Proxy Handler
  • SNS Handler
  • SQS Handler
  • DynamoDB Stream Handler

You can find TypeDocs for this package here.

// handler.ts
import { RestApi } from "effect-lambda";
import { Effect } from "effect";
import { Schema } from "@effect/schema";

export const handler = RestApi.toLambdaHandler(
Effect.succeed({
statusCode: 200,
body: JSON.stringify({ message: "Hello, World!" }),
}),
);

// Or access the payload and path parameters from the event
const PayloadSchema = Schema.Struct({
message: Schema.String,
});
const PathParamsSchema = Schema.Struct({
name: Schema.String,
});
export const handler = RestApi.toLambdaHandler(
RestApi.schemaPathParams(PathParamsSchema).pipe(
Effect.map(({ name }) => name),
Effect.bindTo("name"),
Effect.bind("message", () =>
RestApi.schemaBodyJson(PayloadSchema).pipe(Effect.map((x) => x.message)),
),
Effect.map(({ name, message }) => ({
statusCode: 200,
body: `Hello ${name}, ${message}`,
})),
Effect.catchTag("ParseError", () =>
Effect.succeed({
statusCode: 400,
body: "Invalid JSON",
}),
),
),
);

You can use helmet to secure your application using the provided applyMiddleware utility.

import { applyMiddleware, RestApi } from "effect-lambda";
import helmet from "helmet";
import { Effect, pipe } from "effect";

const toHandler = (effect: Parameters<typeof RestApi.toLambdaHandler>[0]) =>
pipe(effect, Effect.map(applyMiddleware(helmet())), RestApi.toLambdaHandler);

export const handler = Effect.succeed({
statusCode: 200,
body: JSON.stringify({ message: "Hello, World!" }),
}).pipe(toHandler);
import { SQSEvent, toLambdaHandler } from "effect-lambda/Sqs";
import { Effect } from "effect";
export const handler = toLambdaHandler(
SQSEvent.pipe(
Effect.map((event) => {
// Do something with the event
}),
),
);
import { SNSEvent, toLambdaHandler } from "effect-lambda/Sns";
import { Effect } from "effect";
export const handler = toLambdaHandler(
SQSEvent.pipe(
Effect.map((event) => {
// Do something with the event
}),
),
);
// handler.ts
import { toLambdaHandler } from "effect-lambda/DynamoDb";
import { Effect } from "effect";

export const handler = toLambdaHandler(
Effect.map((event) => {
event.Records.forEach((record) => {
// Process each record
console.log("DynamoDB Record: %j", record);
});
}),
);

This handler allows you to process DynamoDB stream events in a functional way using the effect-lambda library. You can access each record in the stream and apply your business logic accordingly.

  • effect - Well you got to have this one to use this library :wink:
    • @effect/schema - Peer dependency of this library for schemas
    • @effect/platform-node - Fully effect native library for network requests, file system, etc.
  • effect-aws - Effect wrapper for common AWS services like S3, DynamoDB, SNS, SQS, etc.

Effect friendly wrapper for AWS Lambdas

  • [x] APIGatewayProxyHandler - REST api or HTTP api with payload version 1
  • [x] SQS Trigger
  • [x] DynamoDB Trigger
  • [x] Utility to deal with an array of records and produce a batchItemFailures response upon failures
  • [x] Authorizer Trigger
  • [x] SNS Trigger
  • [x] Change API naming to use namespaces
  • [x] Add documentation
  • [x] Set up GitHub actions
  • [ ] APIGatewayProxyHandlerV2 - HTTP api with payload version 2
  • [ ] S3 Put Event Handler
  • [ ] S3 Delete Event Handler
  • [ ] SES Trigger
  • [ ] EventBridge Trigger
  • [ ] Add Lambda runtime to allow graceful shutdown and clearing up of resources
  • [ ] Add content negotiation for API Gateway Handlers