# Move an Express route to Lambda without losing its contract.

Carry one HTTP route across a deployment boundary. Protect its semantics, isolate slow work and leave yourself a credible rollback.

By the Effortless Serverless editorial project; prepared 2 November 2026.

## Choose a small, falsifiable migration

Start with a route whose behavior your team can describe from the outside. Consider `POST /reports`: the caller submits a report specification, receives a job reference, and later fetches the result. That description is useful whether Express runs behind a process manager or a function receives an HTTP event. A migration succeeds when the observable agreement survives the move.

Write down the current response status, headers, body schema, authentication decision, timeout and treatment of duplicate submissions. Include an example of a malformed request and a request that is valid but unauthorized. Record a baseline of real latency and errors for this route. Avoid choosing the most trivial health check: it tells you very little about the application’s dependencies.

There are two reasonable paths. Reuse an adapter to keep your existing route stack for an initial cutover, or extract a small handler for an isolated route. An adapter buys continuity but keeps more of the application’s initialization and assumptions. A targeted handler makes the event contract explicit but requires you to reproduce middleware behavior deliberately. Keep unrelated routes on their current host while you test.

## Translate the HTTP boundary explicitly

For an API Gateway HTTP API connected to Lambda, select the payload format version and record that choice next to the handler. The formats describe different request and response shapes. A browser-facing URL is not the function event: parsing cookies, repeated query parameters and encoded payloads deserves a contract test with the actual integration event.

Separate translation from business behavior. A thin adapter validates the incoming event, normalizes the fields you actually need and calls an application function with an ordinary object. The application function should not need to know which field held a gateway request ID. Translate its result back into an explicit HTTP status, content type and response body. Test authentication errors and empty bodies as carefully as the successful JSON response.

**A useful first fixture:** send two requests that differ only in the authenticated tenant. Verify that the same resource ID resolves inside the correct tenant even after the runtime has been reused. Reuse connections where appropriate; keep request-specific identity out of module-level mutable state. Link a redacted request trace to its resulting job ID.

## Decide when the caller is allowed to hear “accepted”

If the existing contract returns a finished report synchronously, changing it to `202 Accepted` changes the client contract. Decide that separately and version the API if needed. For a job-based API, return the job reference only after the system has durably recorded what it has accepted. A background promise running after the response is not a durable queue.

One design stores the job and an outbox entry in a single database transaction, then publishes the outbox event to a queue. Another uses the durable enqueue as the acceptance point and gives the client a stable operation reference. The right choice depends on what must be readable immediately. Do not claim atomicity across separate storage and queue writes unless your design actually supplies it.

Test the awkward interval after a commit but before a response arrives. Have the client retry with the same logical request key; it should recover the existing job reference. A random new key for every HTTP retry defeats this protection. Scope the key to the tenant and operation, and detect a reused key with an incompatible request body.

## Split dispatch, work and replay

Keep the HTTP function responsible for intake. Let a worker perform expensive computation and persist the final state. With SQS as the event source, invocation happens through an event source mapping that polls and can deliver a batch. Acknowledgment, visibility timeout and redrive behavior belong to this boundary; a retry policy for direct asynchronous Lambda invocation is a different mechanism.

Plan for repeat delivery. Write the job’s output under a stable business identifier and use a conditional state change before applying a non-repeatable effect. If a downstream API supports an idempotency token, pass a stable token for that effect. Where a side effect and its completion marker cannot share an atomic commit, document how you detect and reconcile ambiguous success.

A batch with one invalid record should not cause you to charge every valid customer again. Inspect batch response support for your event source; report the failed record identifiers when that mode is enabled and preserve ordering constraints for FIFO work. Keep permanent data errors on a review path with enough redacted context to reproduce the fault. Replay only after fixing the cause.

## Measure the dependency the migration exposes

Moving compute to functions may reveal a bottleneck in a database connection pool, an external API or a tenant-specific rate limit. Count logical jobs, invocation attempts, successful business effects and terminal failures separately. A low HTTP error rate can coexist with an expanding work queue.

Inspect duration, throttles and concurrent executions alongside queue age and application-level completion latency. Compare percentiles and a representative burst, not just average time on an idle system. Verify that structured logs carry a correlation key across intake and worker stages without including secrets or full request bodies.

Try the workload lab with a pessimistic transient-failure rate to see how attempts compound. Its concurrency time is a lower bound for service capacity. It excludes queue polling, retry delays and dependency saturation; use measured values to build an end-to-end target. The model helps surface assumptions; a controlled pilot tests them.

## Cut over with a rollback that respects accepted work

Shift a small, identifiable slice of traffic and define stop conditions before routing it. Examples: a sustained increase in oldest job age, inconsistent downstream writes, or a higher rate of unreconciled submissions. Agree on who can stop intake and where accepted jobs are recorded.

Returning HTTP traffic to Express does not undo queued work. Keep the old and new consumers compatible for the overlap or isolate their queues. Drain or reconcile work accepted by the new route rather than replaying everything blindly. When both stacks agree on final job state, compare results and retire the old path deliberately.

**Why this URL contains a 2018 date**This is a new guide by the Effortless Serverless editorial project, published 2 November 2026. The earlier article at this address was written by Slobodan Stojanović. His [2018 HackerNoon edition](https://medium.com/hackernoon/from-express-js-to-aws-lambda-migrating-existing-node-js-applications-to-serverless-7473041ecc56) identifies the original publication date. The advice above is independently written; it does not imply his endorsement or participation in this edition.

## Primary references

- AWS: HTTP API Lambda integrations: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
- AWS: Lambda best practices: https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html
- AWS: Using Lambda with Amazon SQS: https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html
- AWS: Asynchronous invocation errors and retries: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-error-handling.html
- AWS: Lambda CloudWatch metric types: https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics-types.html
- Slobodan Stojanović: the 2018 article, HackerNoon edition on Medium: https://medium.com/hackernoon/from-express-js-to-aws-lambda-migrating-existing-node-js-applications-to-serverless-7473041ecc56
