lambda-api

repository·main·Indexed 23 days ago

https://github.com/jeremydaly/lambda-api

A lightweight web framework for AWS Lambda serverless applications. It provides routing, middleware, and response handling similar to Express.js, optimized for stateless environments with zero required dependencies. Supports Node 8.10+ and AWS API Gateway Proxy Integration.

Tokens
16.5K
Snippets
43
Records
84
Agent score
80%

What's inside lambda-api

  1. How execution stacks work in Lambda API

    main

    Lambda API uses execution stacks to efficiently process middleware. Stacks are automatically created when you use standard route methods, METHOD(), or use().

    Key behaviors:

    • Inheritance: Execution stacks inherit middleware from matching routes and methods higher up the stack, building a unique final stack for each route.
    • Order Matters: Routes defined before global middleware will not include that middleware in their execution stack. The same applies to wildcard-based routes.
    • Path-based Middleware: Middleware attached to parameterized paths (e.g., /users/:userId) creates mount points. This means a middleware defined for /users/:userId will not execute if the request matches a static path like /users/test unless specifically configured.
    • Debugging: If you use named functions for your middleware, you can inspect the REQUEST.stack property, which returns an array of function names in the order they are executed.
  2. Enable request sampling for tracing

    main

    Sampling allows you to periodically generate log entries for all severities within a request to trace its execution. This is useful for debugging and metrics.

    Global Sampling Configuration

    Set the sampling property in the logger config.

    • target: Minimum number of samples per period.
    • rate: Percentage of samples (0 to 1) to take during the period.
    • period: Duration of the sampling period in seconds.

    Sampling Rules

    You can define specific rules to target certain routes or methods. Each rule requires a route (matching the path definition, e.g., /user/:userId).

    PropertyTypeDescription
    routestringThe route to apply the rule to (Required). Supports wildcards (e.g., /posts/*).
    targetnumberMinimum samples per period.
    ratenumberPercentage of samples to take.
    periodnumberDuration of period in seconds.
    methodstring or arrayHTTP methods to limit the rule to.
    // Sample 2 requests every 30 seconds + 10% of all other requests
    const api = require('lambda-api')({
      logger: {
        sampling: {
          target: 2,
          rate: 0.1,
          period: 30,
        },
      },
    });
    
    // Example with rules: disable sampling for /status, enable for /user and /posts/*
    const apiWithRules = require('lambda-api')({
      logger: {
        sampling: {
          rules: [
            { route: '/status', target: 0, rate: 0 },
            { route: '/user', target: 1, rate: 0.1 },
            { route: '/posts/*', target: 1, rate: 0.1 },
          ],
          target: 0, // disable default target
          rate: 0,   // disable default rate
        },
      },
    });
  3. Configure logging levels and custom levels

    main

    Lambda API uses six standard log levels with default priorities:

    • trace (10)
    • debug (20)
    • info (30)
    • warn (40)
    • error (50)
    • fatal (60)

    Logs are only written if their severity is equal to or higher than the configured level.

    You can define custom levels using the levels property in the logger configuration. This allows you to add new levels or adjust the priority of existing ones.

    // Custom levels example
    const api = require('lambda-api')({
      logger: {
        levels: {
          test: 5, // low priority 'test' level
          customLevel: 35, // between info and warn
          trace: 70, // set trace to the highest priority
        },
      },
    });
  4. Quickstart: Create a simple Lambda API

    main

    To use Lambda API, instantiate it, define your routes using standard HTTP methods, and then pass the Lambda event and context to api.run() within your exported handler.

    Note on AWS SDK versions:

    • lambda-api@v1 uses AWS SDK v3.
    • If your project requires AWS SDK v2, you must use lambda-api@v0.12.0.
    // Require the framework and instantiate it
    const api = require('lambda-api')();
    
    // Define a route
    api.get('/status', async (req, res) => {
      return { status: 'ok' };
    });
    
    // Declare your Lambda handler
    exports.handler = async (event, context) => {
      // Run the request
      return await api.run(event, context);
    };
  5. Return responses using callbacks, return values, or async/await

    main

    Lambda API supports multiple ways to send data back to the user.

    IMPORTANT: You must either use a callback (like res.send()) OR return a value. If you do neither, the execution will hang. Do not return undefined, as the framework will assume no response was intended.

    1. Using Response Callbacks

    Use methods on the res object like send(), json(), or html().

    2. Using Return Values

    Simply return the data from your handler function. The contents will be sent as the body.

    3. Async/Await and Promises

    You can use async handlers and either return the awaited result or use a callback.

    Flow Control Warning

    Callbacks like res.error() or res.send() do not terminate the execution of your function. To prevent subsequent code from running (which might override your error/response), you should return the call to the response method.

    Correct pattern for errors:

    if (condition) {
      return res.error('Error message');
    }
    // Using res.send()
    api.get('/users', (req, res) => {
      res.send({ foo: 'bar' });
    });
    
    // Using return
    api.get('/users', (req, res) => {
      return { foo: 'bar' };
    });
    
    // Using async/await with return
    api.get('/users', async (req, res) => {
      let users = await getUsers();
      return users;
    });
    
    // Using async/await with callback
    api.get('/users', async (req, res) => {
      let users = await getUsers();
      res.send(users);
    });
    
    // Using Promises
    api.get('/users', (req, res) => {
      getUsers().then((users) => {
        res.send(users);
      });
    });
    
    // Correct Flow Control for errors
    api.get('/users', (req, res) => {
      if (req.headers.test === 'test') {
        return res.error('Throw an error');
      }
    
      return { foo: 'bar' };
    });
  6. Run lambda-api benchmarks

    main

    To run the performance comparison suite for lambda-api against other frameworks, navigate to the benchmarks directory and use the provided npm scripts. This suite measures framework overhead (parsing, routing, middleware, and serialization) by running handlers in-process with synthetic API Gateway events.

    Setup and Execution

    cd benchmarks
    npm install
    
    # Print results to stdout
    npm run bench
    
    # Write results to a markdown file
    npm run bench:md
    
    # Write md + json and refresh the main README Benchmarks section
    npm run bench:release
    cd benchmarks
    npm install
    npm run bench
    npm run bench:md
    npm run bench:release
  7. Install S3 helper dependencies

    main

    Lambda API's S3 file helpers (sendFile(), getLink(), and S3 redirects) require the AWS SDK v3. These are declared as optional peer dependencies. If you intend to use these specific features, you must install them manually:

    npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner --save

    If you do not use S3 helpers, you can skip this installation. The S3 client is loaded lazily, allowing bundlers to treat the AWS SDK as external if it is present but unused.

  8. Integrate Lambda API with AWS Application Load Balancer (ALB)

    main

    Lambda API detects the event interface and automatically normalizes the REQUEST object for ALB events. It also handles RESPONSE formatting (supporting both multi-header and non-multi-header modes).

    Key features for ALB:

    • Seamless Integration: You can use the same Lambda function for both API Gateway and ALB without code changes.
    • Binary Support: ALB automatically enables binary support, allowing you to serve images and other binary file types.
    • Routing: Lambda API uses the path parameter from the ALB event for routing. If you use a wildcard in your ALB listener rule, all matching paths are forwarded to the Lambda, where lambda-api handles them using its standard routing (static, parameterized, or wildcard).
  9. Enable binary support in API Gateway

    main

    To support binary data, you must configure your AWS API Gateway settings. Add */* to the Binary Media Types section under API Gateway -> APIs -> [your api] -> Settings.

    Note: Enabling this will cause API Gateway to base64 encode all body content. However, lambda-api will automatically decode it for you before it reaches your handlers.

    */*
  10. Prefix routes using register()

    main

    The register() method allows you to load routes from external modules and apply a prefix to all of them. This is useful for versioning APIs (e.g., /v1/, /v2/) without manually rewriting paths.

    • register() is recursive: nesting register() calls builds cumulative paths (e.g., /v1/v2/path).
    • Prefixed routes are built on top of the instance's base path if one is configured.

    Module Structure for Registered Routes: When using register(), the exported module should be a function that receives (api, opts).

    // routes/v1/products.js
    module.exports = (api, opts) => {
      api.get('/product', handler_v1);
    };
    // handler.js
    const api = require('lambda-api')();
    
    api.register(require('./routes/v1/products'), { prefix: '/v1' });
    api.register(require('./routes/v2/products'), { prefix: '/v2' });
    
    module.exports.handler = (event, context, callback) => {
      api.run(event, context, callback);
    };
  11. Add a new E2E test case

    main

    To add a new test case to the E2E suite, follow the procedure based on the layer you are targeting:

    For Layer 1 (Fast/No Docker):

    1. Create a new fixture directory under e2e/fixtures/<name>/.
    2. Include a package.json and a handler.
    3. The handler must read an event path from process.argv[2] and write the response JSON to stdout.
    4. Wire a check for this new fixture in e2e/run-layer1.mjs.

    For Layer 2 (LocalStack/Real Runtime):

    1. Add a handler under e2e/localstack/handlers/.
    2. Add a corresponding function entry in e2e/localstack/run-all.mjs to handle deployment, invocation, and assertion.