Week 20: Serverless & Edge Functions

Every service this course has built runs as a long-lived Express process — a cluster of workers (Week 14) waiting for requests. That's the right shape for most APIs; it's not the only shape JavaScript runs in production today. This week covers the other end of the spectrum: AWS Lambda functions that run only in response to an event, and edge functions on Cloudflare Workers and Vercel Edge that run your code in hundreds of locations worldwide instead of one region — a genuinely different runtime model, not just a different hosting provider.

Module 17 of 22 Week 20 of 26 ~4–5 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Deploy a Node Lambda function and reason about cold starts and event sources
  • Deploy a Cloudflare Worker or Vercel Edge Function and explain how the edge runtime differs from Node
  • Decide which parts of a system, if any, genuinely belong on serverless or the edge

1. AWS Lambda with Node

A Lambda function is a single handler AWS runs in response to an event and stops — you're billed only for the milliseconds it actually executes, and nothing runs (or costs anything) between invocations. That's the opposite trade-off from the always-on, clustered Express process Week 14 built: no idle cost, but also no persistent process to hold a warm database connection pool or in-memory cache between requests.

src/handlers/hello.ts
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  const name = event.queryStringParameters?.name ?? "world";
  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message: `hello, ${name}` }),
  };
};
terminal — deploying with the AWS CLI
npm run build   # esbuild bundles handler.ts + deps into dist/hello.js
zip -j function.zip dist/hello.js

aws lambda create-function \
  --function-name hello-api \
  --runtime nodejs20.x \
  --role arn:aws:iam::123456789012:role/lambda-basic-execution \
  --handler hello.handler \
  --zip-file fileb://function.zip \
  --timeout 10 \
  --memory-size 256

A cold start happens when Lambda has to initialize a fresh execution environment — download your code, start the Node runtime, run any top-level initialization — before it can handle the first request; a warm invocation reuses an already-initialized environment and skips all of that. A function can be triggered by more than an HTTP request — an S3 upload, a message in an SQS queue, or an EventBridge scheduled rule are all common event sources, each delivering a differently-shaped event object to the same handler signature.

Never open a database connection inside the handler body

Opening a fresh Prisma connection on every invocation adds real latency and can exhaust a database's connection limit under concurrent load. Initialize the Prisma client at module scope, outside the handler function — on a warm invocation, that code doesn't re-run, so the connection is reused across requests, and pairing it with RDS Proxy handles the connection pooling problem at scale.

2. Cloudflare Workers & Vercel Edge Functions

A Lambda function still runs in one AWS region — a request from Tokyo still travels to us-east-1 if that's where you deployed it. Edge functions — Cloudflare Workers, Vercel Edge Functions — run your code in hundreds of points of presence worldwide, executing physically close to whoever made the request, with startup times measured in single-digit milliseconds rather than Lambda's cold-start range.

src/worker.ts — a Cloudflare Worker
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/api/greeting") {
      const name = url.searchParams.get("name") ?? "world";
      return Response.json({ message: `hello, ${name}` });
    }

    return new Response("Not found", { status: 404 });
  },
};

That handler signature — fetch(request, env) — is deliberately not Node's (req, res) Express model at all. Edge runtimes are built on the standard Web Request/Response APIs (the same objects fetch() uses in a browser), not Node's http module, which is why they run in a genuinely different, lighter-weight JavaScript runtime rather than full Node — no fs, no most of Node's built-in modules, no arbitrary npm packages that assume Node APIs exist.

terminal — deploying a Cloudflare Worker
npx wrangler deploy

# ~50ms cold starts, deployed to 300+ locations globally, no region to pick
Prisma's standard client doesn't run on the edge

Prisma's default engine relies on a native binary that isn't available in Cloudflare's or Vercel's edge runtime. Reaching a database from edge code means either Prisma's edge-compatible driver adapters (talking to a database over HTTP, like Neon or PlanetScale's HTTP APIs) or accepting that data-heavy logic stays on a regular Node deployment and only latency-sensitive, mostly-stateless logic runs at the edge.

3. What Changes, and When to Reach for It

Three genuinely different runtime models now sit side by side in this course:

  • Clustered Express (Week 14) — full Node, every npm package available, a persistent process that can hold connections and in-memory state, running in whichever regions you deploy containers to.
  • AWS Lambda — full Node runtime, but no persistent process between invocations; billed per-millisecond, cold starts are a real latency concern, best for event-driven or bursty, infrequent workloads.
  • Edge functions — a restricted Web-standard runtime, not full Node, but running physically close to every user worldwide with near-zero cold starts; best for latency-critical, largely stateless logic (auth checks, A/B test routing, simple API responses, redirects).

The honest posture here is the same one Week 18's DevOps counterpart takes toward Kubernetes vs. serverless: none of these replaces the others by default. A typical real system runs its main API as a clustered Node service, offloads specific event-driven jobs (image processing on S3 upload, a webhook receiver) to Lambda, and pushes only latency-critical, stateless logic (auth middleware, feature-flag evaluation, geo-routing) to the edge.

Choose based on the runtime constraint, not the marketing

"Serverless" and "edge" both promise less operational overhead, but they trade away real capability to get there — Lambda trades persistent state for pay-per-use, edge functions trade the full Node API surface for global latency. Before moving a piece of logic to either, check whether it genuinely needs what's traded away (a database connection pool, an npm package that touches the filesystem) — if it does, it doesn't belong there regardless of how appealing the deployment model sounds.

4. Hands-on Exercise

Hands-on

Deploy one endpoint to Lambda and one to the edge

Build both ends of the serverless spectrum and compare them directly.

Requirements:

  1. Deploy a Lambda function behind an API Gateway route, and measure its cold-start latency vs. warm-invocation latency with a simple timing script.
  2. Add an S3 event source triggering a second Lambda whenever an object is uploaded to a bucket, and confirm it fires by uploading a test file.
  3. Deploy a Cloudflare Worker (or Vercel Edge Function) implementing a simple, stateless endpoint, and confirm it uses standard Request/Response APIs rather than an Express-style handler.
  4. Attempt to import a Node-only package (like fs or your Prisma client directly) into the edge function and observe what breaks — then write a short note on why.
Hint

wrangler dev runs a Cloudflare Worker locally against the real edge runtime (not a Node polyfill), which is the fastest way to discover a Node-only API doesn't exist there before deploying and finding out in production.

5. Knowledge Check

Three quick questions. Expand each to check your answer.

Q1

Why should a database connection be initialized outside the Lambda handler function, not inside it?

Code at module scope only runs once per execution environment, on a cold start; the handler function runs on every invocation. Opening the connection inside the handler means re-establishing it on every single request even when the environment is warm, adding latency and risking exhausting the database's connection limit under load — initializing it at module scope lets a warm invocation reuse the existing connection.

Q2

Why can't an edge function import Node's fs module or use Prisma's default engine directly?

Edge runtimes like Cloudflare Workers implement a restricted, Web-standard JavaScript environment built around fetch-style APIs, not full Node.js — there's no filesystem, and most Node-specific built-in modules simply don't exist there. Prisma's default engine depends on a native binary that has nowhere to run in that environment, which is why reaching a database from edge code requires an edge-compatible driver adapter instead.

Q3

What kind of logic is a genuinely good fit for an edge function, versus logic that should stay on a regular clustered Node service?

Latency-critical, largely stateless logic — auth checks, feature-flag evaluation, geo-based routing, simple redirects — benefits directly from running physically close to every user with near-zero cold starts. Logic that needs a persistent database connection pool, heavy npm dependencies assuming full Node APIs, or significant CPU-bound computation belongs on a regular clustered Node service instead, since the edge runtime's restrictions would work against it.