Read Path and Query Parameters from AWS Lambda and API Gateway

You can build robust serverless REST APIs with the AWS API Gateway and AWS Lambda. Like most APIs, users send data through the request body, URL path, and query string parameters.

The API gateway is the entry point that receives the request, formats the data, applies the mapping, and then forwards the request info to the Lambda Function. The Lambda Function retrieves the request input, executes the logic, and returns the response to the API gateway, which sends it to the client.

Reading the path and query string parameters from a Lambda Function can be tricky. This post will show how to achieve it.

Create a REST API with AWS Lamda & API Gateway using AWS CDK
This post shows how to define RESTful API routes on AWS API Gateway that invoke Lambda functions. We will write the infrastructure stack using the AWS CDK and SAM for local testing.

The use case

You are building an inventory API that exposes endpoints to read products in the inventory. Let's focus on two endpoints:

  • [GET] /products - Find products filtered by the following criteria: name, category, and max price.
  • [GET] /products/{id} - Retrieve a product by its ID.

In the first API endpoint, we provide product filtering criteria as query string parameters, and in the second API endpoint, we provide the product ID as the path parameter.

The architecture diagram of a serverless API.

Prerequisites

To follow this tutorial, make sure you have the following tools installed on your computer.

  • An AWS account, a free tier is enough
  • AWS CLI configured; I wrote a blog post to guide you.
  • Node.js 20 or higher - Read my blog post to install it.
  • Docker is used to build the Lambda function code and test it locally.
  • The AWS CDK v2 and the AWS SAM CLI installed and configured.

Set up the project

I prepared an AWS CDK project with the API gateway and two AWS Lambda infrastructures configured to focus on reading path and query parameters.

The source code can be found on the GitHub repository. Let's clone it and run it locally:


git clone https://github.com/tericcabrel/inventory-tutorial.git

cd inventory-tutorial

yarn install

aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws

yarn cdk synth

If everything is set correctly, you will get the following output:

Set up the AWS CDK project locally.

In the project structure, the folder "src/handlers" two files related to Lambda function handlers:

  • get-product.ts: get a single product by its ID; we will retrieve the ID in request path parameters forwarded by the API Gateway.
  • find-product.ts: find products based on criteria we will retrieve from the request query parameters forwarded by the API Gateway.

Get Path parameters in a Lambda Function

The file "src/handlers/get-product.ts" contains the following code:


import { APIGatewayProxyHandler } from 'aws-lambda';

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  return {
    statusCode: 200,
    headers: {"Content-Type": "text/json"},
    body: JSON.stringify({ message: 'Hello world' }),
  };
};

In the handler function, the argument "event" is an object containing the request information forwarded from the API Gateway. The property "pathParameters" is an object whose key is the path parameter name.

By looking at the API gateway route definition, the path parameter name is "id"


const getProductLambdaIntegration = new apigw.LambdaIntegration(getProductFn);

const productResource = productsResource.addResource("{id}");
productResource.addMethod("GET", getProductLambdaIntegration);

    

To retrieve the path parameter's value in the Lambda Function handler with the code below:


const productId = event.pathParameters?.id;

The question mark called the optional chaining operator is necessary because the "pathParameters" field can be used when the API route does not have a path parameter.

Update the Lambda function code with the following:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { productsData } from '../data/products';

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  const productId = event.pathParameters?.id;

  if (!productId) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'text/json' },
      body: JSON.stringify({ message: 'Product ID is required' }),
    };
  }

  const product = productsData.find((p) => p.id = Number(productId));

  if (!product) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'text/json' },
      body: JSON.stringify({ message: `No product found with the ID "${productId}"` }),
    };
  }

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'text/json' },
    body: JSON.stringify({ data: product }),
  };
};

Validate request path parameters with Zod

In the Lambda Function code, there are two issues:

  1. We checked if the product ID was defined, and we are sure it is. We are doing the check to make the type-checking happy.
  2. The product ID is a number, yet the value received is a string, so we must cast it to a number.

Validating the request parameters path at runtime using Zod will fix these issues. Let's install it.


yarn add zod

The code below defines the Zod schema to validate the request's parameters path.


import { z } from 'zod';

const pathParametersSchema = z.object({
  id: z.number({ message: 'Product ID must be a number', coerce: true }),
});

The Lambda function code looks like this:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { z } from 'zod';
import { productsData } from '../data/products';

const pathParametersSchema = z.object({
  id: z.number({ message: 'Product ID must be a number', coerce: true }),
});

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  const pathParameters = pathParametersSchema.safeParse(event.pathParameters);
  
  if (!pathParameters.success) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'text/json' },
      body: JSON.stringify({ message: pathParameters.error.message }),
    };
  }
  const productId = pathParameters.data.id;

  const product = productsData.find((p) => p.id = productId);

  if (!product) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'text/json' },
      body: JSON.stringify({ message: `No product found with the ID "${productId}"` }),
    };
  }

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'text/json' },
    body: JSON.stringify({ data: product }),
  };
};

Test it by running the API Gateway locally using the AWS SAM CLI.


yarn cdk synth

sam local start-api -t ./cdk.out/InventoryStack.template.json

Make the first call with an invalid product ID and the second one with a valid product ID.


curl -s http://localhost:3000/products/not-a-number | jq
curl -s http://localhost:3000/products/456 | jq

You get the following output.

Call the endpoint to get a product by its ID.

The API Gateway is running on port 3000 at the left terminal console. At right, a first call with an invalid product ID is followed by a second call with a valid one that successfully returns a product.

Get Query parameters in a Lambda Function

The file "src/handlers/find-products.ts" is responsible for retrieving products based on criteria.

In the handler function, the argument "event" is an object containing the request information forwarded from the API Gateway. The property "queryParameters" is an object where each key is a query path parameter.

The API route supports the following query parameters:

  • Name: Search for all products using the name provided.
  • Category: Search for products belonging to the category provided.
  • MaxPrice: Search for products whose prices are lower than the value provided.

The code to retrieve them from the API Gateway request object will look like this:


const name = event.queryStringParameters?.name ?? null;
const category = event.queryStringParameters?.category ?? null;
const maxPrice = event.queryStringParameters?.maxPrice ?? null;
  
console.log(name, category, maxPrice);

  

Validate request query parameters with Zod

The code below defines the Zod schema to validate the request's parameters query string.


import { z } from 'zod';

const queryParametersSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters long').optional(),
  category: z.enum(
    ['electronics', 'foods', 'clothes', 'home'],
    { message: 'Category must be one of "electronics", "foods", "clothes", "home"' }
  ).optional(),
  maxPrice: z.number({ coerce: true }).int().positive().optional(),
}).nullable();

The Lambda function code looks like this:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { z } from 'zod';
import { productsData } from '../data/products';

const queryParametersSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters long').optional(),
  category: z.enum(
    ['electronics', 'foods', 'clothes', 'home'],
    { message: 'Category must be one of "electronics", "foods", "clothes", "home"' }
  ).optional(),
  maxPrice: z.number({ coerce: true }).int().positive().optional(),
}).nullable();

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  const queryParameters = queryParametersSchema.safeParse(event.queryStringParameters);

  if (!queryParameters.success) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'text/json' },
      body: JSON.stringify({ message: queryParameters.error.errors.map((e) => e.message) }),
    };
  }

  const { name, category, maxPrice } = queryParameters.data ?? {};

  const products = productsData.filter((product) => {
    if (name && !product.name.toLowerCase().includes(name.toLowerCase())) {
      return false;
    }

    if (category && product.category !== category) {
      return false;
    }

    if (maxPrice && product.price > maxPrice) {
      return false;
    }

    return true;
  });

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'text/json' },
    body: JSON.stringify({ data: products }),
  };
};

Test it by running the API Gateway locally using the AWS SAM CLI. Make the first call with invalid query string parameters and the second with valid ones.


curl -s http://localhost:3000/products?name=e&maxPrice=-800&category=coding | jq

curl -s http://localhost:3000/products\?name\=elina\&maxPrice\=80000\&category\=electronics | jq

You get the following output.

Call the endpoint to find products by criteria in the query string parameters.

Get Query string parameter array in a Lambda Function

We want to filter products by providing many categories in the query string parameters, which we translate into an array list in the Lambda function.

To provide many values for a query parameter, we do it like this:


http://localhost:3000/products?category=foods&category=home&category=electronics

Which is translated in the code as follows:


{
  "category": ["foods", "home", "electronics"]
}

This key is retrieved in the Lambda function from the event object with the property "multiValueQueryStringParameters," an object in which each key is a query string parameter with an array of values.

The code to retrieve the query string parameter array is the following:


const categories = event.multiValueQueryStringParameters?.category ?? [];

The Zod schema to validate request query string parameters is the following:


const queryParametersSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters long').optional(),
  maxPrice: z.number({ coerce: true }).int().positive().optional(),
}).nullable();

const multiQueryParametersSchema = z.object({
  category: z.array(
    z.enum(
      ['electronics', 'foods', 'clothes', 'home'],
      { message: 'Category must be one of "electronics", "foods", "clothes", "home"' }
    )
  ).min(1, 'At least one category must be provided').optional(),
}).nullable();

The Lambda function code looks like this:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { z } from 'zod';
import { productsData } from '../data/products';

const queryParametersSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters long').optional(),
  maxPrice: z.number({ coerce: true }).int().positive().optional(),
}).nullable();

const multiQueryParametersSchema = z.object({
  category: z.array(
    z.enum(
      ['electronics', 'foods', 'clothes', 'home'],
      { message: 'Category must be one of "electronics", "foods", "clothes", "home"' }
    )
  ).min(1, 'At least one category must be provided').optional(),
}).nullable();

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  const queryParameters = queryParametersSchema.safeParse(event.queryStringParameters);
  const multiQueryParameters = multiQueryParametersSchema.safeParse(event.multiValueQueryStringParameters);

  if (!queryParameters.success) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'text/json' },
      body: JSON.stringify({ message: queryParameters.error.errors.map((e) => e.message) }),
    };
  }

  if (!multiQueryParameters.success) {
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'text/json' },
      body: JSON.stringify({ message: multiQueryParameters.error.errors.map((e) => e.message) }),
    };
  }

  const { name, maxPrice } = queryParameters.data ?? {};
  const { category } = multiQueryParameters.data ?? {};

  const products = productsData.filter((product) => {
    if (name && !product.name.toLowerCase().includes(name.toLowerCase())) {
      return false;
    }

    if (category && !category.includes(product.category)) {
      return false;
    }

    if (maxPrice && product.price > maxPrice) {
      return false;
    }

    return true;
  });

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'text/json' },
    body: JSON.stringify({ data: products }),
  };
};

Start the API Gateway locally and make the following call


curl -s http://localhost:3000/products\?name\=elina\&maxPrice\=80000\&category\=electronics\&category\=home | jq

You get the following output:

Use multi-value query string parameters in the API endpoint.

Get query parameters from a Lambda Function URL

The event object forwarded in the Lambda function handler has the same property as the API Gateway for

  • The query parameters' path
  • The query string parameters

The multivalue query string parameters don't exist.


import type { LambdaFunctionURLHandler } from 'aws-lambda';

export const handler: LambdaFunctionURLHandler = async (event) => {
  
  const pathParameters = event.pathParameters;
  const queryParameters = event.queryStringParameters;
  
  console.log(pathParameters);
  console.log(queryParameters);

  return {
    body: JSON.stringify({ message: 'hello world' }),
    headers: { 'Content-Type': 'text/json' },
    statusCode: 200,
  };
};

Wrap up

This post covered retrieving path and query string parameters in an AWS Lambda function linked to an AWS API gateway. We can summarize it as follows:

  • The property "pathParameters" retrieves path parameters in the API endpoint.
  • The property "queryParameters" retrieves query string parameters in the API endpoint.
  • The property "multiValueQueryStringParameters" retrieves query string parameters with a list of values.

About the Lambda Function URL, reading the request input is the same as with the API Gateway, except there is no property to retrieve multivalue query string parameters.

Additionally, you can validate the path and query string parameters at the runtime to ensure data integrity and get better typing when building features.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.