Code Lambda for fun and profit.
glhf ("Good Lambda Have Function") is a reusable Node.js / ESM library that builds AWS Lambda handler factories with minimal boilerplate, wiring up config loading, an Awilix dependency-injection container, structured logging, and per-event-source parsing, serialization, and error-handling strategies.
glhf exports a family of handler-factory functions, one per AWS Lambda event
source. Each is a thin wrapper around a central createHandler factory so the same
engine serves every event type:
invokeHandler— raw Lambda invocation / arbitrary JSON events (the default / fallback handler). Identity parser and serializer, single-run event strategy.eventbridgeHandler— EventBridge events, with dead-letter-queue redrive support that transparently unwraps both EventBridge-origin and Lambda-onFailureDLQ records.sqsHandler/sqsJsonHandler— SQS batches, processed per record in parallel, each in its own Awilix child scope. The JSON variant also parses each message body as JSON.httpHandler/httpJsonHandler— API Gateway HTTP API v2 proxy events, with Boom-based mapping of processor errors to HTTP status codes. The JSON variant parses the request body and serializes the response body to JSON.
Every handler follows the same lifecycle. On cold start it creates a cache
(cache-manager, 60s TTL) and an Awilix container; per invocation it builds a
request context (ctx), derives a request id, creates a child structured logger,
loads configuration via AWS Config Executor, registers the caller's dependencies,
creates a scoped container, and runs the chosen strategy through a wrapper
that parses the event, invokes the caller's processor, logs start/data/end, and
serializes the result.
The design deliberately separates four pluggable concerns — parser, serializer, wrapper, and strategy — so the same core factory powers every event source. See Handler lifecycle and DI scoping for the full flow and the parent-vs-per-event-vs-per-record scoping model.
glhf is a foundational shared library (published to npm as @pureskillgg/glhf),
not a pipeline stage itself. Virtually every Node.js Lambda across the PureSkill.gg
CS2 pipeline imports it to create its handler export — for example csgo-mutator,
csgo-profile, oauthjs, automatch and link services, AppSync resolvers, and
EventBridge / SQS / HTTP consumers.
It sits alongside the other internal libraries it composes: it loads config with
@pureskillgg/ace (SSM / env descriptors), logs with
@pureskillgg/mlabs-logger, and uses functional utilities from @pureskillgg/phi.
glhf standardizes how all of these services parse events, load config, log, and
handle errors so each consuming repo only writes its own parameters,
registerDependencies, and createProcessor.
This library has no production AWS infrastructure of its own. The serverless.yml,
serverless/*.yml, and handlers/ in this repo exist only as a red/blue
example / integration-test deployment used to exercise the library end to end
(see Example deployment).
This creates a handler that invokes another AWS Lambda function:
import { asClass } from 'awilix'
import { LambdaClient } from '@pureskillgg/awsjs'
import { ssmString } from '@pureskillgg/ace'
import { invokeHandler } from '@pureskillgg/glhf'
const parameters = {
blueLambdaFunction: ssmString('BLUE_LAMBDA_FUNCTION_SSM_PATH')
}
const createProcessor = ({ blueLambdaClient, log }) => async (
event,
container
) => {
return blueLambdaClient.invokeJson(event)
}
const registerDependencies = (container, config) => {
container.register(
'blueLambdaClient',
asClass(LambdaClient).inject(() => ({
name: 'blue',
functionName: config.blueLambdaFunction,
AwsLambdaClient: undefined,
params: undefined
}))
)
}
export const createHandler = invokeHandler({
parameters,
createProcessor,
registerDependencies
})
export const handler = createHandler(parameters)Add this as a dependency to your project using npm with
$ npm install @pureskillgg/glhf
or using Yarn with
$ yarn add @pureskillgg/glhf
A trivial handler that does nothing may be created like this
import { invokeHandler } from '@pureskillgg/glhf'
const createHandler = invokeHandler()
export const handler = createHandler()To create a more useful handler, leverage
parameters, createProcessor, and registerDependencies,
import { invokeHandler } from '@pureskillgg/glhf'
const createHandler = invokeHandler({ createProcessor, registerDependencies })
export const handler = createHandler(parameters)- Use
parametersto load configuration with AWS Config Executor. - Use
registerDependenciesto register things with side-effects that should be mocked out in tests. This function has access to the configuration loaded viaparameters. - Use
createProcessorto define the business logic for handling each event. This function is registered with the Awilix container as a factory function, thus it can access all dependencies registered usingregisterDependencies.
All exported handler functions return a new handler factory with identical signature:
parameters: The AWS Config Executor parameters to load.t: The AVAtobject (if running inside AVA).overrideDependencies: A function with signature(container, config) => voidwhich will be called immediately afterregisterDependencies.
These arguments are all designed to facilitate testing.
See handlers and test/handlers.
The async function returned by the factory has a signature (event, context) => any
matching the signature expected by AWS Lambda.
All handlers execute these steps in order:
- Load the config defined by the parameters.
- Create a new Awilix container and register the default dependencies:
log,reqId, andprocessor. - Parse the event with the parser.
- Execute the processor on the event using the configured strategy and wrapper.
- Serialize and return the result.
The invokeHandler handles AWS Lambda invocation events.
An invocation event is a plain JSON serializable JavaScript object. This handler may thus be used to handle any invocation event if a more specific handler is not provided.
import { invokeHandler } from '@pureskillgg/glhf'
const createProcessor = () => async (event, ctx) => {
return { hello: 'world' }
}
const createHandler = invokeHandler({ createProcessor })
export const handler = createHandler()The eventbridgeHandler handles EventBridge events.
Additionally, this handler supports redriving events from dead-letter
queues connected to both onFailure and event rules.
The full lifecycle of an event is explained below:
- EventBridge invokes the function as an Asynchronous invocation.
- If EventBridge cannot invoke the target function, it will put the event in the DLQ after exhausting the allotted retry attempts.
- If EventBridge can invoke the function it will consider the event delivered, even if the function invocation throws an error.
- If the asynchronous function invocation throws an error, it will put the invocation record in the DLQ after exhausting the allotted retry attempts.
- If the DLQ message is from EventBridge, the message body is the event record.
- If the DLQ message is from Lambda, the message body contains
the event record inside the
requestPayloadattribute.
The example below shows how to configure a Serverless function that will deliver failed messages to a DLQ and allow redriving failed messages with the same function.
import { eventbridgeHandler } from '@pureskillgg/glhf'
const createProcessor = () => async (event, ctx) => {
return { type: event['detail-type'] }
}
const createHandler = invokeHandler({ createProcessor })
export const handler = createHandler()event:
handler: handlers/event.handler
destinations:
onFailure: ${ssm:${self:custom.ssmPrefix}/event_deadletter_queue_arn}
events:
- eventBridge:
eventBus: ${ssm:${self:custom.ssmPrefix}/eventbus_arn}
deadLetterQueueArn: {ssm:${self:custom.ssmPrefix}/event_deadletter_queue_arn}
- sqs:
enabled: false
arn: ${ssm:${self:custom.ssmPrefix}/event_deadletter_queue_arn}
batchSize: 1
The sqsHandler handler handles SQS events.
Since SQS events contain multiple messages, the default strategy for this handler will execute the processor on each message in parallel. Each message will be processed in a child Awilix scope.
The sqsJsonHandler behaves like the sqsHandler except
it will parse the SQS message body as JSON.
The httpHandler handler handles API Gateway Proxy events.
The handler will catch all processor errors, wrap them with Boom, and return a basic status code response. If the processor throws a Boom error, its status code will be respected.
The httpJsonHandler behaves like the httpHandler except
it will parse the request body as JSON, and if the processor
returns an object with a body property, it will
serialize that to JSON and add any missing response properties.
import { sqsJsonHandler } from '@pureskillgg/glhf'
const createProcessor = () => async (event, ctx) => {
return event.body
}
const createHandler = sqsJsonHandler({ createProcessor })
export const handler = createHandler()The handler functions take additional options:
logOptions, parser, serializer, createWrapper, and createStrategy.
These are advanced features which are stable, but not yet fully documented.
They are used internally to create the included handler factories.
Please refer to the code for how they may be used.
A parser is a synchronous function which transforms the raw AWS Lambda event before it is passed to the processor.
A serializer is a synchronous function which transforms the output of the processor into the final return value of the AWS Lambda function.
Parsers and serializers should be agnostic to details of user input and response content. They are not expected to throw runtime errors. If a parser or serializer throws, it indicates a bug in its implementation, or a bad configuration (e.g., trying to parse payloads for the wrong event type).
A wrapper function must call a strategy with the event and context. It may optionally call the logger, parser, and serializer.
A strategy has access to the Awilix container scoped to the event. It should resolve and call the processor and optionally handle errors.
Every handler factory is composed from the same internal primitives. These are the
real exported names confirmed in lib; consuming services usually only need
the handler factories, but the pieces are exported for advanced use and custom handlers.
| Export | Source | Role |
|---|---|---|
invokeHandler |
lib/handlers/invoke.js |
Raw invocation / generic JSON; identity parser + serializer, event strategy |
eventbridgeHandler |
lib/handlers/eventbridge.js |
EventBridge events; eventbridge parser + parallel strategy; DLQ redrive |
sqsHandler / sqsJsonHandler |
lib/handlers/sqs.js |
SQS batches; per-record parallel processing in child scopes |
httpHandler / httpJsonHandler |
lib/handlers/http.js |
API Gateway HTTP API v2; HTTP strategy maps errors to status codes |
createHandler |
lib/handlers/factory.js |
Central engine all factories delegate to (cache, container, config, scope, run) |
| Export | Behavior |
|---|---|
createEventStrategy |
Resolves and runs the processor once; resolves an optional onError |
createParallelStrategy |
Validates the parsed event is an array; runs the processor on each record concurrently (Promise.all), each in its own child scope |
createHttpStrategy |
Catches processor errors, boomifies them, logs a warning, returns { statusCode } |
| Export | Role |
|---|---|
createInvokeWrapper |
Orchestrates parse → strategy → serialize with start/data/end logging |
createRecordWrapper |
Per-record inner loop for the parallel strategy (own ctx/log per record) |
- Parsers:
identityParser,sqsParser/sqsJsonParser,eventbridgeParser,recordsParser,apiGatewayProxyParser/apiGatewayProxyJsonParser. These normalize raw events — typing SQS attributes (String / Number / Binary), unwrapping DLQ records, and validating API Gateway v2 (version2.0) payloads. - Serializers:
identitySerializer,apiGatewayProxySerializer/apiGatewayProxyJsonSerializer. The JSON variant stringifiesbodyand fills in default HTTP response fields.
createCtx (lib/ctx.js), getRequestId (lib/request-id.js),
discoverEventType (lib/event/discover.js — distinguishes aws:sqs from everything
else), createLogger / logFatal (lib/logger.js), createGetConfig wrapping ace's
getConfig (lib/config.js), createScope / registerContext (lib/container.js),
createCache (lib/cache.js, 60s TTL), readJson (lib/fs.js), and securityHeaders
(lib/headers.js).
glhf is a published library, not a deployed service, so there is no application AWS
infrastructure to inspect — no DynamoDB, SQS, SNS, Step Functions, EventBridge,
AppSync, or CloudFront in this repo. The observability behavior below is what the library
establishes for consuming services; find runtime logs in the consuming Lambda's own
CloudWatch log groups (/aws/lambda/<consumer-service>-<stage>-<function>).
- Structured logs. Every handler emits structured logs via
@pureskillgg/mlabs-logger. The factory seeds a child logger with env metadata and AWS request context, derives areqId(from SQS message attributes,event.reqId, or a fresh uuid), and logs start / data / end around each processor run. Verbosity is controlled byLOG_LEVEL(defaults toinfo); the consuming service also setsLOG_ENV,LOG_SYSTEM,LOG_SERVICE, andLOG_VERSION. To trace a single invocation, filter the consumer's log group by itsreqId. - Errors. Synchronous invoke errors propagate to the caller. HTTP handlers convert
errors to status codes with
@hapi/boom(createHttpStrategy). glhf itself wires no DLQ or retry sink — those belong to the consuming service's Serverless config; theeventbridgeHandleris what lets a service redrive its own EventBridge /onFailureDLQ messages (see the deep-dive below). - Sentry. glhf does not bundle Sentry; consuming services typically wrap their handler
with
@sentry/serverless. The example deploy in this repo demonstrates that pattern.
The serverless.yml + serverless/*.yml + handlers/ exist only as an
integration test of the library, deployed under service glhf (default stage stg):
glhf-${sls:stage}-red(handlers/red.handler) — readsBLUE_LAMBDA_FUNCTION_SSM_PATHvia ace'sssmStringand invokes the blue Lambda through@pureskillgg/awsjsLambdaClient.glhf-${sls:stage}-blue(handlers/blue.handler, envRANK=ge) — the downstream target invoked by red.- SSM parameter
/pureskillgg/glhf/${stage}/glhf/blue_lambda_function(AWS::SSM::Parameter, typeString,Value: Ref BlueLambdaFunctioninserverless/resources.yml) — holds the deployed blue function; red reads it and IAM grantsssm:GetParameteron/${owner}/${app}/${stage}/${name}/*. - Example handlers are Sentry-wrapped (
Sentry.AWSLambda.wrapHandler, with theSentryNodeServerlessSDK:40Lambda layer) purely to exercise the consumer pattern.
Log groups for the example deploy (30-day retention): /aws/lambda/glhf-${sls:stage}-red
and /aws/lambda/glhf-${sls:stage}-blue.
Note: the
httpApiblock inserverless.ymlhas its default endpoint disabled and no function attaches anhttpApievent, so no API Gateway is actually provisioned for the example.
- Handler lifecycle and DI scoping — the cold-start →
per-invocation → per-record flow, the parent / per-event / per-record Awilix scope
model, and how EventBridge DLQ redrive unwrapping works. Start here if a consuming
service's dependencies or
reqIdare not what you expect.
$ git clone https://github.com/pureskillgg/glhf.git
$ cd glhf
$ nvm install
$ yarn install
Run the command below in a separate terminal window:
$ yarn run test:watch
Primary development tasks are defined under scripts in package.json
and available via yarn run.
View them with
$ yarn run
The source code is hosted on GitHub. Clone the project with
$ git clone git@github.com:pureskillgg/glhf.git
You will need Node.js with npm, Yarn, and a Node.js debugging client.
Be sure that all commands run under the correct Node version, e.g., if using nvm, install the correct version with
$ nvm install
Set the active version for each shell session with
$ nvm use
Install the development dependencies with
$ yarn install
Use the npm version command to release a new version.
This will push a new git tag which will trigger a GitHub action.
Publishing may be triggered using on the web using a version workflow_dispatch on GitHub Actions.
Serverless deployment is triggered by a release repository_dispatch on GitHub Actions.
Deployment may be triggered using on the web using a release workflow_dispatch on GitHub Actions.
GitHub Actions should already be configured: this section is for reference only.
The following repository secrets must be set on GitHub Actions:
NPM_TOKEN: npm token for publishing packages.AWS_DEFAULT_REGION: The AWS region Serverless will deploy to.AWS_ACCESS_KEY_ID: AWS access key ID.AWS_SECRET_ACCESS_KEY: AWS secret access key.GH_TOKEN: A personal access token that can trigger workflows.DISCORD_WEBHOOK: The Discord webhook to notify on deploy.SENTRY_AUTH_TOKEN: The Sentry auth token.
These must be set manually.
The version and format GitHub actions require a user with write access to the repository. including access to trigger workflows and read and write packages.
Set these additional secrets to enable the action:
GH_USER: The GitHub user's username.GH_TOKEN: A personal access token for the user.GIT_USER_NAME: The GitHub user's real name.GIT_USER_EMAIL: The GitHub user's email.GPG_PRIVATE_KEY: The GitHub user's GPG private key.GPG_PASSPHRASE: The GitHub user's GPG passphrase.
Please submit and comment on bug reports and feature requests.
To submit a patch:
- Fork it (https://github.com/pureskillgg/glhf/fork).
- Create your feature branch (
git checkout -b my-new-feature). - Make changes.
- Commit your changes (
git commit -am 'Add some feature'). - Push to the branch (
git push origin my-new-feature). - Create a new Pull Request.
This npm package is licensed under the MIT license.
Portions of code in this project were taken and adapted from @meltwater/jackalambda which is licensed under the MIT license.
This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright holder or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.