For me, the strangler fig pattern is the most honest answer to the question “How do we get off our legacy system without stopping everything?”. After all, almost every team has a candidate like that: an application that has been running for years, that nobody likes to touch, and that still earns money every single day. However, the reflex is often a big rewrite. In my experience, that rewrite surprisingly often ends with two systems that both need maintenance.
So in this fifth part of the series, I show you the other route: you put a facade in front of the legacy system and move one feature after another to Lambda. Every step is small, and every step can be rolled back. The example is a made-up bike rental called “Radhafen”, built with the Serverless Framework and TypeScript.
In short
With the strangler fig pattern, every request passes through a facade, here an HTTP API in API Gateway. One route after another moves to Lambda, and everything else keeps flowing to the legacy system through the $default route. The biggest stumbling block is rarely the routes. It is the data that old and new both need.
What you will learn in this article
- How the strangler fig pattern works and when it pays off
- Why an HTTP API with a $default route makes a good facade for a legacy system
- Comparing a feature with Lambda safely using a parallel run
- Switching over and rolling back with a single parameter
- Which mistakes around data, host names and costs are typical
What is the strangler fig pattern?
The name comes from Martin Fowler. He saw strangler figs in the rain forests of Queensland: plants that sprout high up in a tree, send their roots down along the trunk and wrap around their host over the years. Eventually, the fig stands on its own and the old tree is gone. Applied to software, this means you build the new system around the old one and gradually take over its responsibilities until you can switch it off.
Fowler's reasoning is rather sober. A full replacement often fails because nobody can fully describe the existing behavior, because the business won't wait two years for new features, and because part of the old behavior isn't needed anymore anyway. He sums it up in one sentence: “the reduced risk and earlier value from the gradual approach outweigh its costs.” So the gradual route costs extra, but it pays off through lower risk and earlier benefits.
The three building blocks
Technically, you need three things. First, a facade: a proxy that every request passes through and that decides whether the old or the new system answers. Second, a seam: a spot in the legacy system where a feature can be cut out cleanly. Third, transitional architecture, meaning code you only need during the migration and delete afterwards. That includes, for example, a comparison mode or an adapter that keeps data in sync between old and new.
This transitional code feels like waste at first. I see it more like scaffolding: nobody would renovate a house without scaffolding just because it gets taken down at the end.
Four phases: facade, shadow comparison, live switch, retiring the legacy system.
When does the strangler fig pattern pay off?
Not every migration needs this pattern. For a small application with three endpoints, a rewrite over a weekend is often faster than any facade. The AWS guide to cloud design patterns agrees: the bigger and more tangled the legacy system, the more the gradual approach is worth.
In practice, the pattern fits well if at least two of these points apply: the legacy system is large and poorly documented. Also, the business keeps needing new features while you migrate. Outages or a “freeze” lasting several months are not an option. The external interface is HTTP, so you can put a proxy in front of it. It gets hard, on the other hand, when the legacy system mainly talks to other systems through a shared database. Then there is no door to put the facade in front of.
What about Refactor Spaces?
With Migration Hub Refactor Spaces, AWS had a service that built exactly this kind of facade for you. According to the AWS guide, Refactor Spaces has not been open to new customers since November 7, 2025. As an alternative, AWS points to AWS Transform there. The good news: the facade itself is so little infrastructure that you can build it yourself with a few lines in the Serverless Framework. That's exactly what we'll do now.
The example: a bike rental with a legacy system
Radhafen rents out city bikes, e-bikes and cargo bikes. Its application runs as a single Node application on one server. It can list stations, create reservations, write invoices and calculate prices. It is reachable at https://legacy.radhafen.example.
As the first seam, I pick price calculation, so POST /quotes. In fact, it is a textbook candidate: it doesn't read data from other modules, writes nothing, gets called often and is easy to test. Reservations, in contrast, depend on stations, payments and invoices. They come much later, once the facade has proven itself.
My rule of thumb for the order: first features without side effects and with their own data, then new features built directly in the new system, and core processes with shared data at the very end. That way you get to know the facade before a mistake gets expensive.
How does the HTTP API become a facade?
The facade is an HTTP API in API Gateway. For our purpose, two rules from the AWS documentation matter. First, the $default route catches all requests that don't match any other route and passes the full path on to the integration. Second, the most specific route always wins: an exact match like POST /quotes beats a route with {proxy+}, and that one beats $default.
In other words, that's the whole pattern. The $default route points to the legacy system through an HTTP proxy integration. In addition, every feature you move gets its own route to a Lambda function. Anything without its own route automatically ends up at the legacy system. If you delete a route again, traffic falls back just as automatically. That's your built-in emergency exit.
🎬 Strangler fig pattern: routing through the facade – this animated diagram only runs on my site: watch it on sascha-ms.de
In case you're wondering why I use an HTTP API here and not a REST API: for plain forwarding, HTTP APIs are cheaper and simpler. I covered the differences in detail in REST API vs HTTP API: Choosing API Gateway.
Step by step: implementing the strangler fig pattern with Lambda
Prerequisites
- Node.js 22 and an AWS account with permissions for Lambda, API Gateway, CloudFormation and CloudWatch Logs
- Serverless Framework v4 (
npm i -g serverless); if you are still on v3, Migrate to Serverless Framework v4 will help - A legacy system that is publicly reachable over HTTPS (for private systems, see the section on security)
- Basic TypeScript knowledge
1. Create the project
The project is deliberately small. It needs no runtime dependencies because Node.js 22 ships with fetch. The tests run on Node's built-in test runner.
mkdir -p radhafen-strangler/src radhafen-strangler/test
cd radhafen-strangler
# create package.json and tsconfig.json as shown below, then:
npm install
{
"name": "strangler-fig-lambda",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "node --experimental-strip-types --test test/*.test.ts",
"deploy": "serverless deploy"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.164",
"@types/node": "^22.20.5",
"typescript": "^7.0.2"
}
}
{
"compilerOptions": {
"target": "ES2023",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src", "test"]
}
2. The facade in the Serverless Framework
For every function with an httpApi event, the Serverless Framework automatically creates an HTTP API. Its logical CloudFormation ID is HttpApi. The framework can't create an integration to an arbitrary URL directly; according to the docs, httpApi only offers the proxy integration to Lambda. That's why we add the integration and the $default route as plain CloudFormation resources. We pass the target URL and the mode as parameters, so you can switch later without changing code.
# serverless.yml
service: radhafen-strangler
frameworkVersion: '4'
params:
default:
legacyBaseUrl: https://legacy.radhafen.example
migrationMode: shadow
provider:
name: aws
runtime: nodejs22.x
architecture: arm64
region: eu-central-1
memorySize: 256
timeout: 10
logRetentionInDays: 14
environment:
LEGACY_BASE_URL: ${param:legacyBaseUrl}
MIGRATION_MODE: ${param:migrationMode}
functions:
quotes:
handler: src/quote-handler.handler
events:
- httpApi:
method: POST
path: /quotes
resources:
Resources:
# Everything without its own route goes to the legacy system unchanged.
LegacyIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref HttpApi
IntegrationType: HTTP_PROXY
IntegrationMethod: ANY
IntegrationUri: ${param:legacyBaseUrl}
PayloadFormatVersion: '1.0'
TimeoutInMillis: 29000
LegacyDefaultRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref HttpApi
RouteKey: $default
Target: !Join ['/', ['integrations', !Ref LegacyIntegration]]
Two details matter. First, HTTP proxy integrations use payload format 1.0, which is why it is set explicitly. Second, the integration timeout is 29 seconds, because an HTTP API doesn't allow more than 30 seconds anyway. If your legacy system has endpoints that take longer, you'll find out now at the latest. For those cases, take a look at the job pattern with 202 and SQS from part 1.
3. The new pricing logic
The new logic is a pure function without network access. That makes it easy to test and keeps it cleanly separated from the transitional code. I calculate amounts in cents to avoid floating-point rounding errors.
// src/tariff.ts
// New pricing logic for the “Radhafen” bike rental – a pure function without I/O.
export type BikeType = 'city' | 'ebike' | 'cargo';
export interface QuoteRequest {
bikeType: BikeType;
hours: number;
member: boolean;
}
export interface Quote {
bikeType: BikeType;
hours: number;
totalCents: number;
currency: 'EUR';
}
// Prices in cents to avoid floating-point rounding errors.
const HOURLY_CENTS: Record<BikeType, number> = { city: 300, ebike: 600, cargo: 800 };
const DAY_CAP_CENTS: Record<BikeType, number> = { city: 1800, ebike: 3500, cargo: 4500 };
const MEMBER_DISCOUNT = 0.1;
export function parseQuoteRequest(body: unknown): QuoteRequest | string {
if (typeof body !== 'object' || body === null) return 'Body must be a JSON object';
const b = body as Record<string, unknown>;
if (b.bikeType !== 'city' && b.bikeType !== 'ebike' && b.bikeType !== 'cargo') {
return 'bikeType must be city, ebike or cargo';
}
if (typeof b.hours !== 'number' || !Number.isInteger(b.hours) || b.hours < 1 || b.hours > 72) {
return 'hours must be an integer from 1 to 72';
}
return { bikeType: b.bikeType, hours: b.hours, member: b.member === true };
}
export function computeQuote(req: QuoteRequest): Quote {
const fullDays = Math.floor(req.hours / 24);
const restHours = req.hours % 24;
const restCents = Math.min(restHours * HOURLY_CENTS[req.bikeType], DAY_CAP_CENTS[req.bikeType]);
let total = fullDays * DAY_CAP_CENTS[req.bikeType] + restCents;
if (req.member) total = Math.round(total * (1 - MEMBER_DISCOUNT));
return { bikeType: req.bikeType, hours: req.hours, totalCents: total, currency: 'EUR' };
}
4. Parallel run: comparing old and new
Now comes the part I find most valuable in migrations. Before the new logic serves real answers, it runs along in shadow mode. A parallel run means: the Lambda function still asks the legacy system, also calculates the result itself and compares both. The customer gets the legacy answer, and you get a log entry with every difference.
In this way, you find the rules nobody ever wrote down. Maybe the legacy system rounds differently, maybe the daily cap only applies from the eighth hour. A parallel run shows you this with real traffic, without anyone seeing a wrong price. There is one limitation: it only works for requests without side effects. You must not create a reservation twice just to compare.
// src/compare.ts
// Compares the legacy response with the new calculation (parallel run).
export interface Difference {
field: string;
legacy: unknown;
next: unknown;
}
export function diffResults(legacy: Record<string, unknown>, next: Record<string, unknown>): Difference[] {
const fields = new Set([...Object.keys(legacy), ...Object.keys(next)]);
const diffs: Difference[] = [];
for (const field of fields) {
if (JSON.stringify(legacy[field]) !== JSON.stringify(next[field])) {
diffs.push({ field, legacy: legacy[field], next: next[field] });
}
}
return diffs;
}
// src/legacy-client.ts
// Small client for the legacy system. The timeout is deliberately below the Lambda timeout.
export interface LegacyResponse {
status: number;
body: unknown;
}
export async function callLegacy(
baseUrl: string,
path: string,
payload: unknown,
timeoutMs = 3000,
): Promise<LegacyResponse> {
const res = await fetch(new URL(path, baseUrl), {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-strangler-shadow': '1' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(timeoutMs),
});
const text = await res.text();
let body: unknown = text;
try {
body = JSON.parse(text);
} catch {
// Not JSON – pass the text through unchanged.
}
return { status: res.status, body };
}
The client also sets the x-strangler-shadow header. As a result, the legacy system can recognize these calls in its own metrics. The three-second timeout is deliberately well below the Lambda timeout, so the function can respond cleanly instead of being cut off hard. If the legacy system hangs regularly, the circuit breaker from part 4 is the right addition.
5. The handler with shadow and live mode
// src/quote-handler.ts
// POST /quotes – the first feature extracted from the legacy system.
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
import { computeQuote, parseQuoteRequest } from './tariff.ts';
import { diffResults } from './compare.ts';
import { callLegacy } from './legacy-client.ts';
type Mode = 'shadow' | 'live';
const json = (statusCode: number, body: unknown): APIGatewayProxyStructuredResultV2 => ({
statusCode,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
export async function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyStructuredResultV2> {
const mode: Mode = process.env.MIGRATION_MODE === 'live' ? 'live' : 'shadow';
const legacyBaseUrl = process.env.LEGACY_BASE_URL ?? '';
let raw: unknown;
try {
raw = JSON.parse(event.body ?? '');
} catch {
return json(400, { message: 'Invalid JSON' });
}
const parsed = parseQuoteRequest(raw);
if (typeof parsed === 'string') return json(400, { message: parsed });
const next = computeQuote(parsed);
if (mode === 'live') {
console.log(JSON.stringify({ msg: 'quote-served', source: 'lambda', mode }));
return json(200, next);
}
// Shadow mode: the legacy system stays the source of truth, the new logic only runs for comparison.
try {
const legacy = await callLegacy(legacyBaseUrl, '/quotes', raw);
if (legacy.status !== 200 || typeof legacy.body !== 'object' || legacy.body === null) {
console.log(JSON.stringify({ msg: 'parallel-run', match: false, reason: 'legacy-status', status: legacy.status }));
return json(legacy.status, legacy.body);
}
const diffs = diffResults(legacy.body as Record<string, unknown>, { ...next });
console.log(JSON.stringify({ msg: 'parallel-run', match: diffs.length === 0, diffs }));
return json(200, legacy.body);
} catch (err) {
console.log(JSON.stringify({ msg: 'parallel-run', match: false, reason: 'legacy-error', error: String(err) }));
return json(502, { message: 'Legacy system unreachable' });
}
}
In shadow mode, the legacy system stays the source of truth. In live mode, only Lambda answers and the legacy system is no longer called for this route. You switch via the MIGRATION_MODE environment variable, which comes from the migrationMode parameter.
6. Testing
The tests replace fetch with a stub. That lets you check all three paths without a real legacy system: a difference in shadow mode, live mode without a legacy call, and the behavior when the legacy system doesn't answer.
// test/tariff.test.ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { computeQuote, parseQuoteRequest } from '../src/tariff.ts';
test('hourly price for a city bike', () => {
assert.equal(computeQuote({ bikeType: 'city', hours: 3, member: false }).totalCents, 900);
});
test('daily cap applies from the seventh hour', () => {
assert.equal(computeQuote({ bikeType: 'city', hours: 7, member: false }).totalCents, 1800);
});
test('members get a 10 percent discount', () => {
assert.equal(computeQuote({ bikeType: 'ebike', hours: 26, member: true }).totalCents, 4230);
});
test('invalid input is rejected', () => {
assert.match(String(parseQuoteRequest({ bikeType: 'tandem', hours: 2 })), /bikeType/);
assert.match(String(parseQuoteRequest({ bikeType: 'city', hours: 0 })), /hours/);
});
// test/quote-handler.test.ts
import { test, beforeEach, mock } from 'node:test';
import assert from 'node:assert/strict';
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
import { handler } from '../src/quote-handler.ts';
import { diffResults } from '../src/compare.ts';
const event = (body: unknown) => ({ body: JSON.stringify(body) }) as APIGatewayProxyEventV2;
const logs: string[] = [];
beforeEach(() => {
logs.length = 0;
mock.restoreAll();
mock.method(console, 'log', (line: string) => logs.push(line));
process.env.LEGACY_BASE_URL = 'https://legacy.radhafen.example';
});
test('shadow mode returns the legacy response and reports differences', async () => {
process.env.MIGRATION_MODE = 'shadow';
mock.method(globalThis, 'fetch', async () =>
new Response(JSON.stringify({ bikeType: 'city', hours: 7, totalCents: 2100, currency: 'EUR' }), { status: 200 }),
);
const res = await handler(event({ bikeType: 'city', hours: 7 }));
assert.equal(res.statusCode, 200);
assert.equal(JSON.parse(res.body!).totalCents, 2100);
const log = JSON.parse(logs[0]);
assert.equal(log.match, false);
assert.deepEqual(log.diffs, [{ field: 'totalCents', legacy: 2100, next: 1800 }]);
});
test('live mode no longer calls the legacy system', async () => {
process.env.MIGRATION_MODE = 'live';
const fetchMock = mock.method(globalThis, 'fetch', async () => new Response('{}'));
const res = await handler(event({ bikeType: 'cargo', hours: 2, member: true }));
assert.equal(res.statusCode, 200);
assert.equal(JSON.parse(res.body!).totalCents, 1440);
assert.equal(fetchMock.mock.callCount(), 0);
});
test('unreachable legacy system returns 502', async () => {
process.env.MIGRATION_MODE = 'shadow';
mock.method(globalThis, 'fetch', async () => {
throw new Error('timeout');
});
const res = await handler(event({ bikeType: 'ebike', hours: 1 }));
assert.equal(res.statusCode, 502);
});
test('invalid JSON returns 400', async () => {
const res = await handler({ body: '{broken' } as APIGatewayProxyEventV2);
assert.equal(res.statusCode, 400);
});
test('diffResults only finds real differences', () => {
assert.deepEqual(diffResults({ a: 1, b: [1, 2] }, { a: 1, b: [1, 2] }), []);
assert.equal(diffResults({ a: 1 }, { a: 1, c: 'new' }).length, 1);
});
npm run typecheck
npm test
Expected result: tsc reports nothing, and the test runner shows # pass 9 and # fail 0.
7. Deploy and evaluate shadow mode
serverless deploy --param="legacyBaseUrl=https://legacy.radhafen.example" --param="migrationMode=shadow"
# Example calls (use the URL from the deploy output)
curl https://abc123.execute-api.eu-central-1.amazonaws.com/stations
curl -X POST https://abc123.execute-api.eu-central-1.amazonaws.com/quotes \
-H "content-type: application/json" \
-d '{"bikeType":"ebike","hours":3,"member":true}'
After the deployment, the framework prints the URL of the HTTP API. A call to /stations ends up at the legacy system, a POST /quotes at Lambda, which still returns the old answer in shadow mode. For the comparison, open CloudWatch Logs Insights, pick the function's log group and count matches and differences:
filter @message like /"msg":"parallel-run"/
| parse @message '"match":*,' as match
| stats count(*) as total by match
Let this run for a few days with real traffic, ideally across a weekend and a month-end. Every difference is either a bug in the new logic or a rule in the legacy system that you deliberately want to keep or drop. That decision belongs to the business side, not to the development team alone.
8. Switch over and roll back
Once the differences are explained, you switch. If something goes wrong after all, the way back is the same command with the old value. For emergencies there is a second brake: if you comment out the function's httpApi event and deploy, the route disappears and traffic falls back to the legacy system through $default.
# Switch over: Lambda answers POST /quotes on its own
serverless deploy --param="legacyBaseUrl=https://legacy.radhafen.example" --param="migrationMode=live"
# Roll back: return to shadow mode
serverless deploy --param="legacyBaseUrl=https://legacy.radhafen.example" --param="migrationMode=shadow"
Only when the route has been stable in live mode for a few weeks do you delete the shadow code and the endpoint in the legacy system. That's part of the transitional architecture. If you forget this step, you end up with three variants of the same logic.
The hard half: data in the strangler fig pattern
Price calculation was simple on purpose. As soon as you move features that write data, things get serious. For example, the reservation in the new system may need stations from the old database, and the old invoicing feature may need reservations from the new one. Three tools have proven themselves for this phase.
The first is an anti-corruption layer. It's an adapter that translates the terms and data formats of the legacy system into those of the new one, so old models don't leak into the new code. The AWS guide describes it as a facade inside the legacy system through which existing callers reach the new service without knowing about it. It gets deleted once nobody needs it anymore.
The second is synchronization through events. When the new service changes its data, it publishes an event, and a small synchronizer writes the change into the old database. That means you deliberately accept eventual consistency: for a short moment, old and new see different states. How to produce such events reliably without losing one is shown by the outbox pattern from part 2.
The third is more of a ban: no dual writes. If a function writes to the old and the new database at the same time, sooner or later one write will fail and the other won't. Then you have two truths and no way to merge them again. For every data area, define exactly one owner at any point in time.
Typical mistakes and how to avoid them
Absolute URLs and redirects
Many legacy systems build links and redirects from the host name they see in the request. Behind the facade, that can suddenly be the internal name, and your users land on legacy.radhafen.example after logging in. So before switching over, test logins, redirects, cookies and emails with links on purpose. It's boring, and it saves you exactly the evening when everything would otherwise be on fire.
First steps that are too big
It's tempting to move the entire reservation module right away because it “has to be rebuilt anyway”. However, that throws away the advantage of the pattern. A good first cut goes live within one or two sprints and can be rolled back with a single command.
The facade as the next monolith
A facade should route, not calculate. As soon as business logic, data transformation or special cases for individual customers end up there, you're building your next monolith, this time in YAML. In short, logic belongs in the Lambda functions, and the facade only knows paths.
No end in sight
The underestimated risk is organizational. After the first three quick wins, the migration becomes a never-ending task, and the legacy system lives on for another five years. Besides the technical side, Fowler explicitly says the organization has to change too, so this work is sustained over time. That's why I would set a visible goal from day one, for example the share of requests the legacy system still receives by the end of a given quarter.
Cost, latency and security
Once the facade is active, every request passes through API Gateway, including those the legacy system still answers. So you pay for traffic that used to go only through your load balancer, and every call gets an extra network hop. Therefore, do a rough calculation of both before you start. For the Lambda part, the Lambda cost calculator helps; you'll find API Gateway prices on the AWS pricing page.
In shadow mode, you also double the load for the migrated feature, because the legacy system keeps calculating and Lambda calculates on top. For cheap price calculation, that doesn't matter. For expensive queries, you should only compare a share of requests, say every tenth one.
On security: an HTTP proxy integration needs a publicly reachable target. If your legacy system runs in a private network, use a private integration via a VPC link to an Application Load Balancer, Network Load Balancer or Cloud Map service instead. In both cases, make sure nobody can bypass the facade and call the legacy system directly. Otherwise you end up with two entrances, and only one of them has your new protections, such as the Lambda authorizer for HTTP APIs.
My take
I consider the strangler fig pattern one of the few architecture patterns that is mainly a leadership tool. Above all, it makes progress visible, spreads risk across many small decisions and gives the team its first win after just a few weeks. For motivation, that often matters more than any target architecture on a slide.
Where I disagree is the common idea that you need to know the perfect target architecture up front. In fact, the first route teaches you more about your legacy system than three workshops. What I would clarify beforehand, though, is the data question: which area belongs to whom, and when? Leave that open, and you'll migrate the routes quickly and then get stuck on the data for a year.
FAQ
Where does the name strangler fig pattern come from?
A strangler fig is a plant that grows around a tree and eventually replaces it. The pattern describes replacing a legacy system gradually, with the new system taking over one feature at a time.
Do I need microservices for the strangler fig pattern?
No. The target can also be a modular monolith or a handful of Lambda functions. What matters is the facade that routes between old and new.
What is the difference between strangler fig and big bang?
With a big bang, the new system is built completely and switched over on a fixed date. With the strangler fig pattern, both systems run in parallel, and each part is moved and can be rolled back on its own.
Does this also work with a REST API or a load balancer?
Yes. REST APIs, an Application Load Balancer with path rules or CloudFront with multiple origins can all serve as the facade. The HTTP API is just particularly lean for getting started.
Your next step
Take your own legacy system and look for the one feature that writes no data, gets called often and is easy to test. That's your first seam. Next, build the facade with just this one route in shadow mode and watch the differences for a week. After that, you'll know more about your legacy system than after any architecture workshop.
Over to you: which feature in your legacy system would you extract first, and what has been holding you back so far? Let me know in the comments.
More in this series: Overview “Serverless patterns in practice” · Previous part: Lambda circuit breaker · Next part: Saga pattern with Step Functions
Sources
- Martin Fowler: Strangler Fig
- AWS Prescriptive Guidance: Strangler fig pattern
- AWS docs: Routes for HTTP APIs ($default, priority)
- AWS docs: HTTP proxy integrations for HTTP APIs
- AWS docs: Quotas for HTTP APIs
- Serverless Framework: HTTP API events
- Tested on October 28, 2026 with Node.js 22.23, TypeScript 7.0.2 and @types/aws-lambda 8.10.164:
tscand the 9 tests from the article. The CloudFormation resources generated for the facade were checked withserverless package. The URL in the curl examples is a placeholder.
This post was created with AI assistance.
👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser.
Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

Top comments (0)