Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ familiar will accelerate your development, allowing you to focus on your busines

## Usage

Here's a simple example to get you up and running quickly (assumes your execution environment
is Node 12.x):
Here's a simple example to get you up and running quickly:

`npm i @silvermine/lambda-express`

Expand Down Expand Up @@ -65,19 +64,31 @@ app.get('/my-endpoint', async (request: Request, response: Response) => {
response.send('Hello world!');
});

export const handler = (event: RequestEvent, context: Context, callback: Callback): void => {
app.run(event, context, callback);
};
// Use the helper function to create an async handler (Recommended for Node.js 18+)
export const handler = createAsyncHandler(app);

export default handler;
// Or use runAsync directly:
//
// export const handler = async (event: RequestEvent, context: Context): Promise<ResponseResult> => {
// return app.runAsync(event, context);
// };

// Or for Node.js <24.x, use the callback style:
//
// export const handler = (event: RequestEvent, context: Context, callback: Callback): void => {
// app.run(event, context, callback);
// };
```

At this point you should be able to compile, bundle, and deploy this Lambda.
Assuming you have configured APIGW or ALB to forward traffic to your Lambda,
you will now have a very basic working API!

**Note:** Both handler styles (async/await and callback) are supported for Node.js <24.x.
However, the async/await style is recommended for Node.js 18+ and is required for Node.js
24+.

## License

This software is released under the MIT license. See [the license file](LICENSE) for more
details.

24 changes: 23 additions & 1 deletion src/Application.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Callback, Context } from 'aws-lambda';
import Router from './Router';
import { RequestEvent, HandlerContext } from './request-response-types';
import { RequestEvent, HandlerContext, ResponseResult } from './request-response-types';
import { isUndefined, StringUnknownMap, Writable } from '@silvermine/toolbox';
import { Request, Response } from '.';
import { isErrorWithStatusCode } from './interfaces';
Expand Down Expand Up @@ -85,6 +85,9 @@ export default class Application extends Router {
* @param evt The event provided to the Lambda handler
* @param context The context provided to the Lambda handler
* @param cb The callback provided to the Lambda handler
*
* @deprecated Use `runAsync()` instead. Callback-style handlers are deprecated in
* Node.js 24.
*/
public run(evt: RequestEvent, context: Context, cb: Callback): void {
const req = new Request(this, evt, this._createHandlerContext(context)),
Expand All @@ -100,6 +103,25 @@ export default class Application extends Router {
});
}

/**
* Run the app for a Lambda invocation using async/await pattern.
*
* @param evt The event provided to the Lambda handler
* @param context The context provided to the Lambda handler
* @returns A Promise that resolves with the response result or rejects with an error
*/
public runAsync(evt: RequestEvent, context: Context): Promise<ResponseResult> {
return new Promise((resolve, reject) => {
this.run(evt, context, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result as ResponseResult);
}
});
});
}

private _createHandlerContext(context: Context): HandlerContext {
const newContext: Writable<HandlerContext> = {
functionName: context.functionName,
Expand Down
22 changes: 22 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Application from './Application';
import Request from './Request';
import Response from './Response';
import Router from './Router';
import { Context } from 'aws-lambda';
import { RequestEvent, ResponseResult } from './request-response-types';

export {
Application,
Expand All @@ -10,6 +12,26 @@ export {
Router,
};

/**
* Creates an async Lambda handler function from an Application instance.
*
* @param app The Application instance to use for handling requests
* @returns An async Lambda handler function
*
* @example
* ```typescript
* const app = new Application();
* app.get('/hello', (req, res) => res.send('Hello World!'));
*
* export const handler = createAsyncHandler(app);
* ```
*/
export function createAsyncHandler(app: Application): (event: RequestEvent, context: Context) => Promise<ResponseResult> {
return async (event: RequestEvent, context: Context): Promise<ResponseResult> => {
return app.runAsync(event, context);
};
}

// We need to export only types that are used in public interfaces (e.g. those used in
// concrete classes like Application, Request, Response, Router, exported above).
export {
Expand Down
138 changes: 137 additions & 1 deletion tests/integration-tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
apiGatewayRequestRawQuery,
} from './samples';
import { spy, SinonSpy, assert } from 'sinon';
import { Application, Request, Response, Router } from '../src';
import { Application, Request, Response, Router, createAsyncHandler } from '../src';
import { RequestEvent, ResponseResult } from '../src/request-response-types';
import { NextCallback, IRoute, IRouter, ErrorWithStatusCode } from '../src/interfaces';
import { expect } from 'chai';
Expand Down Expand Up @@ -925,4 +925,140 @@ describe('integration tests', () => {
expect(cb.firstCall.args[1].body).to.eql('URIError handled by error handler');
});

describe('runAsync method', () => {

it('returns a Promise that resolves with the response result', async () => {
app.get('/hello', (_req: Request, resp: Response): void => {
resp.json({ message: 'Hello World' });
});

const evt = makeRequestEvent('/hello'),
result = await app.runAsync(evt, handlerContext());

expect(result.statusCode).to.eql(200);
expect(result.body).to.eql(JSON.stringify({ message: 'Hello World' }));
expect(result.isBase64Encoded).to.eql(false);
expect(result.multiValueHeaders['Content-Type']).to.eql([ 'application/json; charset=utf-8' ]);
});

it('returns a Promise that rejects when an error occurs', async () => {
app.get('/error', (_req: Request, _resp: Response): void => { // eslint-disable-line @typescript-eslint/no-unused-vars
const err: ErrorWithStatusCode<Error> = new Error('Test error') as ErrorWithStatusCode<Error>;

err.statusCode = 500;
throw err;
});

const evt = makeRequestEvent('/error');

let errorThrown = false;

try {
await app.runAsync(evt, handlerContext());
} catch(err) {
errorThrown = true;
}

// Since the error is caught by the last resort handler, it should resolve, not
// reject
expect(errorThrown).to.eql(false);
});

it('works with async middleware and handlers', async () => {
app.use(async (_req: Request, resp: Response, next: NextCallback): Promise<void> => {
resp.append('X-Async-Middleware', 'ran');
next();
});

app.get('/async', async (_req: Request, resp: Response): Promise<void> => {
resp.json({ async: true });
});

const evt = makeRequestEvent('/async'),
result = await app.runAsync(evt, handlerContext());

expect(result.statusCode).to.eql(200);
expect(result.body).to.eql(JSON.stringify({ async: true }));
expect(result.multiValueHeaders['X-Async-Middleware']).to.eql([ 'ran' ]);
});

it('works with ALB events', async () => {
app.get('/alb-test', (_req: Request, resp: Response): void => {
resp.send('ALB response');
});

const evt = makeRequestEvent('/alb-test', albRequest()),
result = await app.runAsync(evt, handlerContext());

expect(result.statusCode).to.eql(200);
expect(result.statusDescription).to.eql('200 OK');
expect(result.body).to.eql('ALB response');
});

it('returns 404 when no matching route is found', async () => {
const evt = makeRequestEvent('/nonexistent'),
result = await app.runAsync(evt, handlerContext());

expect(result.statusCode).to.eql(404);
});

});

describe('createAsyncHandler helper', () => {

it('creates an async handler that works correctly', async () => {
app.get('/test', (_req: Request, resp: Response): void => {
resp.json({ test: 'success' });
});

const handler = createAsyncHandler(app),
evt = makeRequestEvent('/test'),
result = await handler(evt, handlerContext());

expect(result.statusCode).to.eql(200);
expect(result.body).to.eql(JSON.stringify({ test: 'success' }));
});

it('creates a handler that works with middleware', async () => {
app.use((_req: Request, resp: Response, next: NextCallback): void => {
resp.append('X-Middleware', 'executed');
next();
});

app.get('/middleware-test', (_req: Request, resp: Response): void => {
resp.send('OK');
});

const handler = createAsyncHandler(app),
evt = makeRequestEvent('/middleware-test'),
result = await handler(evt, handlerContext());

expect(result.statusCode).to.eql(200);
expect(result.body).to.eql('OK');
expect(result.multiValueHeaders['X-Middleware']).to.eql([ 'executed' ]);
});

it('creates a handler that properly handles errors', async () => {
app.get('/error-test', (_req: Request, _resp: Response): void => { // eslint-disable-line @typescript-eslint/no-unused-vars
throw new Error('Test error');
});

const handler = createAsyncHandler(app),
evt = makeRequestEvent('/error-test'),
result = await handler(evt, handlerContext());

// Error is caught by last resort handler
expect(result.statusCode).to.eql(500);
});

it('creates a handler that returns 404 for non-existent routes', async () => {
const handler = createAsyncHandler(app),
evt = makeRequestEvent('/does-not-exist'),
result = await handler(evt, handlerContext());

expect(result.statusCode).to.eql(404);
});

});

});