Node.js Serverless on AWS Lambda · HTTP APIs on Lambda

API Gateway, HTTP APIs and Function URLs

Choosing the front door for a Lambda API, and the event-format mismatch that makes handlers read undefined.

Scenario

Example app: a team migrates an API from an API Gateway REST API to an HTTP API to cut costs. The Lambda handlers are unchanged.

What you see in production

After the switch, every request returns 400 "missing route" or behaves as if it were a GET. Logs show method: undefined and path: undefined. Nothing in the function code changed.

The naive code

The handler reads the REST API (payload format 1.0) fields:

export const handler = async (event) => {
  const method = event.httpMethod;          // only in payload format 1.0
  const path = event.path;
  return route(method, path, event.body);
};

Reproduce it

The same request arrives in the two payload formats; the naive handler only understands one:

Payload format 1.0 versus 2.0Try it yourself
const v1 = { version: "1.0", httpMethod: "POST", path: "/orders", headers: { "content-type": "application/json" }, body: "{\"sku\":\"A1\"}" };
const v2 = { version: "2.0", routeKey: "POST /orders", rawPath: "/orders",
  requestContext: { http: { method: "POST", path: "/orders" } }, headers: { "content-type": "application/json" }, body: "{\"sku\":\"A1\"}" };

const naive = (event) => ({ method: event.httpMethod, path: event.path });
const normalize = (event) => event.version === "2.0"
  ? { method: event.requestContext.http.method, path: event.rawPath }
  : { method: event.httpMethod, path: event.path };

console.log("naive  v1:", naive(v1));
console.log("naive  v2:", naive(v2));
console.log("fixed  v2:", normalize(v2));

Output

naive  v1: { method: 'POST', path: '/orders' }
naive  v2: { method: undefined, path: undefined }
fixed  v2: { method: 'POST', path: '/orders' }

Root cause

API Gateway REST APIs send payload format 1.0; HTTP APIs default to payload format 2.0 (with requestContext.http.method, rawPath, cookies as an array, and differences in headers and query strings); Lambda function URLs use the 2.0 format too. Handlers written against one shape silently read undefined from the other.

The fix

Choose the front door deliberately, and parse events through one tested adapter (or a framework adapter) rather than ad-hoc field access:

  • HTTP API: lower cost and latency, JWT authorizers, CORS, simple routing. Good default for most new APIs.
  • REST API: request validation, usage plans and API keys, caching, AWS WAF integration, private APIs, more transformation features.
  • Function URL: a built-in HTTPS endpoint for one function, with IAM or no auth; no routing, throttling or WAF on its own (put CloudFront in front for those). Good for webhooks and internal tools, and it supports response streaming.
import { z } from "zod";

const Order = z.object({ sku: z.string().min(1), qty: z.number().int().positive() });

export const handler = async (event) => {
  const { method, path } = normalize(event);   // one adapter for both payload formats
  if (method !== "POST" || path !== "/orders") return json(404, { error: "not found" });
  const parsed = Order.safeParse(JSON.parse(event.body ?? "{}"));
  if (!parsed.success) return json(400, { error: "invalid order", issues: parsed.error.issues });
  return json(201, await createOrder(parsed.data));
};

Frameworks such as Hono, or adapters for Express/Fastify, also normalise events, at the cost of a little extra code at init.

Trade-offs

  • HTTP APIs are cheaper but lack some REST API features; adding them back (WAF via CloudFront, validation in code) has its own cost.
  • One "Lambda-lith" handling every route keeps cold starts shared and code simple; one function per route gives finer permissions and scaling at the cost of more deployment units.
  • Framework adapters ease migration from Express but add init time.

Detect it in production

  • Contract tests that replay sample events of each payload format against the handler in CI.
  • Alarms on 4xx/5xx rates per route right after infrastructure changes.
  • Log the payload version field at debug level when diagnosing.

Key points

  • REST APIs use payload format 1.0; HTTP APIs and function URLs use 2.0 by default.
  • Normalise events in one adapter; test with real sample events.
  • HTTP API for most new APIs; REST API when you need its extra features; function URLs for simple endpoints.

Further reading: Choose between REST APIs and HTTP APIs · HTTP API Lambda integrations and payload formats · Lambda function URLs

Practice questions

A handler works behind a REST API but reads undefined for the method behind an HTTP API. Why?

Answer

REST APIs send payload format 1.0 (event.httpMethod, event.path) while HTTP APIs default to 2.0 (event.requestContext.http.method, event.rawPath). Normalise both formats in one adapter or set the HTTP API integration to payload format 1.0.

When would you choose a REST API over an HTTP API?

Answer

When you need REST-only features such as request validation, usage plans and API keys, response caching, direct AWS WAF integration, private APIs, or advanced request and response transformations. Otherwise HTTP APIs are cheaper and simpler.

What are Lambda function URLs good for, and what do they lack?

Answer

A quick HTTPS endpoint for a single function, useful for webhooks, internal tools and response streaming. They have no routing, throttling, usage plans or WAF on their own; put CloudFront in front if you need caching, WAF or a custom domain.

Open in the app to mark lessons complete, take notes and track your progress.