Quarkus AWS Lambda HTTP: Live-Code Serverless on Localhost
Use the mock event server to keep the Quarkus dev loop, test raw API Gateway HTTP API events, and prove the packaged Lambda with SAM CLI before AWS.
Serverless tutorials often ask you to slow down before you write anything useful. Install SAM. Start Docker. Generate some YAML. Wait for a container. Change one line. Start again.
That is fine when you want full Lambda emulation. It is annoying when you just want to build an HTTP endpoint and keep the edit-save-refresh loop you already have with Quarkus.
The useful part of Quarkus AWS Lambda HTTP is not only the deployment packaging. It is the local development model. In dev and test mode, Quarkus starts a mock event server that converts normal HTTP requests into API Gateway events and feeds them into the Lambda poll loop. You still hit http://localhost:8080, but under the hood Quarkus is already pretending to be API Gateway and Lambda. The AWS Lambda HTTP guide documents that local flow, the raw /_lambda_ endpoint, and the AWS event types you can inject directly.
So we build a small quote API, run it through the mock event server, push a handcrafted API Gateway V2 event into /_lambda_, test the whole thing, and package it into the ZIP and SAM files you would actually hand to AWS.
What we build
We build a Quarkus app called lambda-http-localhost with two normal REST endpoints:
GET /quotes/{destination}for a quick quote driven by path, query, and header dataPOST /quotes/previewfor the same quote flow with a JSON body
Then we do four things that matter for this topic:
Run the app in dev mode and call it like an ordinary HTTP service on
localhost:8080Read Lambda-specific request metadata through
APIGatewayV2HTTPEvent.RequestContextPOST a raw API Gateway HTTP API event to
/_lambda_Package the app into
function.zip,sam.jvm.yaml, andsam.native.yaml
The sample is intentionally small. The point is the Lambda HTTP adapter and the local loop, not the business domain.
What you need
JDK 21 or newer
Maven 3.9+
Optional: AWS SAM CLI and Podman if you want the final container-based local run
About one ☕️
There is one version detail worth calling out: As of June 7, 2026, the official AWS Lambda runtime list includes both java21 and java25. On this sample, Quarkus still generated Runtime: java21 in target/sam.jvm.yaml, so I pinned maven.compiler.release to 21 to keep the generated ZIP compatible with that template. If your generated project already targets 21, keep it. If you want to move the Lambda runtime to 25, own that change explicitly instead of hoping the defaults agree.
Create the project
Generate the project with REST plus the Lambda HTTP adapter or grab the full copy from my Github repository:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.mainthread \
-DprojectArtifactId=lambda-http-localhost \
-Dextensions='rest-jackson,amazon-lambda-http' \
-DnoCode
cd lambda-http-localhostThe generated pom.xml is close to normal Quarkus application wiring. For this walkthrough, make sure the compiler target stays on Java 21:
<properties>
<maven.compiler.release>21</maven.compiler.release>
<quarkus.platform.version>3.36.1</quarkus.platform.version>
</properties>The only two application dependencies we need are these:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-amazon-lambda-http</artifactId>
</dependency>quarkus-rest-jackson gives us normal Quarkus REST JSON endpoints. quarkus-amazon-lambda-http is the bridge that turns those endpoints into something AWS Lambda plus API Gateway HTTP API can invoke.
Add the quote model
Start with plain records for the request and response plus one small error payload:
package com.mainthread.lambdahttp;
public record ShippingQuoteRequest(String destination, int weightGrams, String speed, String customerTier) {
}package com.mainthread.lambdahttp;
public record ShippingQuoteResponse(
String destination,
String speed,
String customerTier,
int weightGrams,
int quotedCents,
int estimatedBusinessDays,
String fulfillmentRegion,
String gatewayRequestId,
String stage) {
}package com.mainthread.lambdahttp;
public record ErrorResponse(String message) {
}I keep the response a little more verbose than a real quote API would. That is deliberate. We want to see Lambda-specific metadata like gatewayRequestId and stage in the output.
The speed model is a tiny enum:
package com.mainthread.lambdahttp;
import java.util.Arrays;
enum ShippingSpeed {
STANDARD("standard", 4, 0),
EXPRESS("express", 2, 350),
OVERNIGHT("overnight", 1, 900);
private final String value;
private final int estimatedBusinessDays;
private final int surchargeCents;
ShippingSpeed(String value, int estimatedBusinessDays, int surchargeCents) {
this.value = value;
this.estimatedBusinessDays = estimatedBusinessDays;
this.surchargeCents = surchargeCents;
}
String value() {
return value;
}
int estimatedBusinessDays() {
return estimatedBusinessDays;
}
int surchargeCents() {
return surchargeCents;
}
static ShippingSpeed from(String rawSpeed) {
String normalized = rawSpeed == null ? "" : rawSpeed.trim().toLowerCase();
return Arrays.stream(values())
.filter(speed -> speed.value.equals(normalized))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unsupported speed '" + rawSpeed
+ "'. Use standard, express, or overnight."));
}
}Nothing Lambda-specific yet. That is the point. Most of the code should still look like a normal Quarkus app.
Add the pricing service
The service does four practical things:
normalizes destination and customer tier input
validates a simple weight range
calculates the quote
fills in fallback Lambda metadata when the request context does not provide values
Create src/main/java/com/mainthread/lambdahttp/ShippingQuoteService.java:
package com.mainthread.lambdahttp;
import java.util.Locale;
import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ShippingQuoteService {
private static final Set<String> EU_DESTINATIONS = Set.of(
"amsterdam", "barcelona", "berlin", "lisbon", "madrid", "paris", "porto", "rome", "vienna");
public ShippingQuoteResponse preview(String destination,
int weightGrams,
String rawSpeed,
String rawCustomerTier,
String gatewayRequestId,
String stage) {
String normalizedDestination = normalizeDestination(destination);
ShippingSpeed speed = ShippingSpeed.from(rawSpeed);
String customerTier = normalizeCustomerTier(rawCustomerTier);
int normalizedWeight = normalizeWeight(weightGrams);
int quotedCents = quoteCents(normalizedWeight, speed, customerTier);
return new ShippingQuoteResponse(
normalizedDestination.toUpperCase(Locale.ROOT),
speed.value(),
customerTier,
normalizedWeight,
quotedCents,
speed.estimatedBusinessDays(),
fulfillmentRegion(normalizedDestination),
defaultIfBlank(gatewayRequestId, "mock-event"),
defaultIfBlank(stage, "$default"));
}
private String normalizeDestination(String destination) {
if (destination == null || destination.isBlank()) {
throw new IllegalArgumentException("Destination is required.");
}
return destination.trim().toLowerCase(Locale.ROOT);
}
private int normalizeWeight(int weightGrams) {
if (weightGrams < 100 || weightGrams > 5000) {
throw new IllegalArgumentException("weightGrams must be between 100 and 5000.");
}
return weightGrams;
}
private String normalizeCustomerTier(String rawCustomerTier) {
String normalized = defaultIfBlank(rawCustomerTier, "standard").toLowerCase(Locale.ROOT);
return switch (normalized) {
case "standard", "silver", "gold" -> normalized;
default -> throw new IllegalArgumentException(
"Unsupported customer tier '" + rawCustomerTier + "'. Use standard, silver, or gold.");
};
}
private int quoteCents(int weightGrams, ShippingSpeed speed, String customerTier) {
int base = 700;
int weightCharge = Math.max(0, weightGrams - 500) / 100 * 45;
int tierDiscount = switch (customerTier) {
case "silver" -> 120;
case "gold" -> 240;
default -> 0;
};
return base + weightCharge + speed.surchargeCents() - tierDiscount;
}
private String fulfillmentRegion(String destination) {
return EU_DESTINATIONS.contains(destination) ? "eu-central" : "global-export";
}
private String defaultIfBlank(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}
}The only thing here that belongs to the Lambda story is the fallback for gatewayRequestId and stage. In normal localhost requests, the mock event server supplies those values. In a direct raw event post, we can set them ourselves. The service just keeps the final output stable.
Add the Lambda-aware REST resource
Now wire the REST endpoints in QuoteResource.java:
package com.mainthread.lambdahttp;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
@Path("/quotes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class QuoteResource {
private final ShippingQuoteService shippingQuoteService;
public QuoteResource(ShippingQuoteService shippingQuoteService) {
this.shippingQuoteService = shippingQuoteService;
}
@GET
@Path("/{destination}")
public ShippingQuoteResponse previewFromRequest(
@PathParam("destination") String destination,
@QueryParam("speed") @DefaultValue("standard") String speed,
@QueryParam("weightGrams") @DefaultValue("500") int weightGrams,
@HeaderParam("X-Customer-Tier") @DefaultValue("standard") String customerTier,
@Context APIGatewayV2HTTPEvent.RequestContext requestContext) {
return shippingQuoteService.preview(
destination,
weightGrams,
speed,
customerTier,
requestId(requestContext),
stage(requestContext));
}
@POST
@Path("/preview")
public ShippingQuoteResponse previewFromBody(
ShippingQuoteRequest request,
@Context APIGatewayV2HTTPEvent.RequestContext requestContext) {
return shippingQuoteService.preview(
request.destination(),
request.weightGrams(),
request.speed(),
request.customerTier(),
requestId(requestContext),
stage(requestContext));
}
@ServerExceptionMapper
RestResponse<ErrorResponse> mapIllegalArgument(IllegalArgumentException exception) {
return RestResponse.status(RestResponse.Status.BAD_REQUEST, new ErrorResponse(exception.getMessage()));
}
private String requestId(APIGatewayV2HTTPEvent.RequestContext requestContext) {
return requestContext == null ? null : requestContext.getRequestId();
}
private String stage(APIGatewayV2HTTPEvent.RequestContext requestContext) {
return requestContext == null ? null : requestContext.getStage();
}
}This is the line that makes the article worth writing:
@Context APIGatewayV2HTTPEvent.RequestContext requestContextThe Quarkus guide documents that for the HTTP API adapter you can inject Context, APIGatewayV2HTTPEvent, or APIGatewayV2HTTPEvent.RequestContext directly into Quarkus REST resources. That means you can keep writing REST endpoints and still inspect Lambda or API Gateway metadata when you need it.
Run it in dev mode
Start Quarkus dev mode:
./mvnw quarkus:devThe interesting log lines are the Lambda ones:
Mock Lambda Event Server Started
The amazon-lambda dev service is ready to accept connections on localhost:8080/_lambda_
Listening on: http://localhost:8080/_lambda_/2018-06-01/runtime/invocation/nextYou still call localhost:8080, but Quarkus is already routing through the Lambda event server.
Call the GET endpoint:
curl -s 'http://localhost:8080/quotes/lisbon?speed=express&weightGrams=900' \
-H 'X-Customer-Tier: gold' | jqYou should see:
{
"destination": "LISBON",
"speed": "express",
"customerTier": "gold",
"weightGrams": 900,
"quotedCents": 990,
"estimatedBusinessDays": 2,
"fulfillmentRegion": "eu-central",
"gatewayRequestId": "mock-event",
"stage": "$default"
}The business fields are ours. gatewayRequestId and stage come from the Lambda-shaped request context Quarkus synthesized locally.
Call the POST path too:
curl -s http://localhost:8080/quotes/preview \
-H 'Content-Type: application/json' \
-d '{
"destination": "Chicago",
"weightGrams": 1200,
"speed": "overnight",
"customerTier": "silver"
}' | jqExpected response:
{
"destination": "CHICAGO",
"speed": "overnight",
"customerTier": "silver",
"weightGrams": 1200,
"quotedCents": 1795,
"estimatedBusinessDays": 1,
"fulfillmentRegion": "global-export",
"gatewayRequestId": "mock-event",
"stage": "$default"
}Prove the live-coding part
Change one line in ShippingQuoteService while dev mode is still running:
case "gold" -> 240;Set it to:
case "gold" -> 300;Save the file and run the first curl again. The quote drops from 990 to 930 without a restart because Quarkus recompiles the class and the mock event server keeps feeding requests through the same Lambda development loop. Change it back after the experiment so the rest of this walkthrough still matches.
That is the real value here. You keep the same local speed as a normal Quarkus REST app, but the request path is already Lambda-shaped.
Push a raw API Gateway V2 event into /_lambda_
Normal localhost requests are enough most of the time. The raw /_lambda_ path is for the cases where you want to simulate a more exact event shape.
Create an event.json file like this:
{
"version": "2.0",
"routeKey": "$default",
"rawPath": "/quotes/porto",
"rawQueryString": "speed=overnight&weightGrams=900",
"headers": {
"X-Customer-Tier": "gold"
},
"requestContext": {
"routeKey": "$default",
"stage": "qa",
"apiId": "local-test",
"domainName": "localhost",
"requestId": "req-raw-42",
"http": {
"method": "GET",
"path": "/quotes/porto",
"protocol": "HTTP/1.1",
"sourceIp": "127.0.0.1",
"userAgent": "curl"
}
}
}Now POST it to the mock event server:
curl -s -X POST http://localhost:8080/_lambda_ \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
--data @event.json | jqThe response looks different:
{
"statusCode": 200,
"headers": {
"content-length": "206",
"Content-Type": "application/json;charset=UTF-8"
},
"multiValueHeaders": null,
"cookies": null,
"body": "{\"destination\":\"PORTO\",\"speed\":\"overnight\",\"customerTier\":\"gold\",\"weightGrams\":900,\"quotedCents\":1540,\"estimatedBusinessDays\":1,\"fulfillmentRegion\":\"eu-central\",\"gatewayRequestId\":\"req-raw-42\",\"stage\":\"qa\"}",
"isBase64Encoded": false
}When you hit /_lambda_, you are no longer talking to your REST endpoint directly. You are talking to the Lambda adapter, so you get an API Gateway-style response object back. Your business JSON is in the body field.
This is also where the @Context APIGatewayV2HTTPEvent.RequestContext injection is visible. The response now shows gatewayRequestId as req-raw-42 and stage as qa, because those came from the handcrafted event instead of the default mock values.
Test the same flow in Quarkus
Quarkus starts the mock event server in test mode too. The guide documents the default test port as 8081, and REST Assured is wired to it automatically in @QuarkusTest.
The normal endpoint tests look like this:
package com.mainthread.lambdahttp;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
@QuarkusTest
@TestHTTPEndpoint(QuoteResource.class)
class QuoteResourceTest {
@Test
void shouldPreviewQuoteThroughMockEventServer() {
given()
.header("X-Customer-Tier", "gold")
.queryParam("speed", "express")
.queryParam("weightGrams", 900)
.when()
.get("/lisbon")
.then()
.statusCode(200)
.body("destination", equalTo("LISBON"))
.body("speed", equalTo("express"))
.body("customerTier", equalTo("gold"))
.body("weightGrams", equalTo(900))
.body("quotedCents", equalTo(990))
.body("estimatedBusinessDays", equalTo(2))
.body("fulfillmentRegion", equalTo("eu-central"));
}
@Test
void shouldPreviewQuoteFromJsonBody() {
String payload = """
{
"destination": "Chicago",
"weightGrams": 1200,
"speed": "overnight",
"customerTier": "silver"
}
""";
given()
.contentType(ContentType.JSON)
.body(payload)
.when()
.post("/preview")
.then()
.statusCode(200)
.body("destination", equalTo("CHICAGO"))
.body("speed", equalTo("overnight"))
.body("customerTier", equalTo("silver"))
.body("weightGrams", equalTo(1200))
.body("quotedCents", equalTo(1795))
.body("estimatedBusinessDays", equalTo(1))
.body("fulfillmentRegion", equalTo("global-export"));
}
@Test
void shouldRejectUnsupportedSpeed() {
given()
.queryParam("speed", "teleport")
.when()
.get("/porto")
.then()
.statusCode(400)
.body("message", containsString("Unsupported speed"));
}
}The raw event test hits /_lambda_ directly:
package com.mainthread.lambdahttp;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import java.util.Map;
import org.junit.jupiter.api.Test;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
@QuarkusTest
class LambdaEventServerTest {
@Test
void shouldAcceptRawApiGatewayEvent() {
APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent();
event.setVersion("2.0");
event.setRouteKey("$default");
event.setRawPath("/quotes/porto");
event.setRawQueryString("speed=overnight&weightGrams=900");
event.setHeaders(Map.of("X-Customer-Tier", "gold"));
APIGatewayV2HTTPEvent.RequestContext.Http http = new APIGatewayV2HTTPEvent.RequestContext.Http();
http.setMethod("GET");
http.setPath("/quotes/porto");
http.setProtocol("HTTP/1.1");
http.setSourceIp("127.0.0.1");
http.setUserAgent("rest-assured");
APIGatewayV2HTTPEvent.RequestContext requestContext = new APIGatewayV2HTTPEvent.RequestContext();
requestContext.setHttp(http);
requestContext.setRequestId("req-raw-42");
requestContext.setRouteKey("$default");
requestContext.setStage("qa");
requestContext.setApiId("local-test");
requestContext.setDomainName("localhost");
event.setRequestContext(requestContext);
given()
.contentType(ContentType.JSON)
.accept(ContentType.JSON)
.body(event)
.when()
.post("/_lambda_")
.then()
.statusCode(200)
.body("statusCode", equalTo(200))
.body("body", containsString("\"gatewayRequestId\":\"req-raw-42\""))
.body("body", containsString("\"stage\":\"qa\""))
.body("body", containsString("\"quotedCents\":1540"));
}
}Run the tests with:
./mvnw testIf you also want the packaged-app path, add small integration test shells that extend the JVM tests:
@QuarkusIntegrationTest
class QuoteResourceIT extends QuoteResourceTest {
}@QuarkusIntegrationTest
class LambdaEventServerIT extends LambdaEventServerTest {
}The Quarkus guide calls out that the mock event server works for @QuarkusIntegrationTest too, which is the practical reason these tiny extension-style IT classes are worth keeping around.
Package it for Lambda
Build the project:
./mvnw package -DskipTestsOn this sample, the build produced these files:
ls -lh target/function.zip target/sam.jvm.yaml target/sam.native.yaml-rw-r--r-- 1 you staff 17M target/function.zip
-rw-r--r-- 1 you staff 873B target/sam.jvm.yaml
-rw-r--r-- 1 you staff 945B target/sam.native.yamlThe generated JVM SAM file is the more interesting one:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: AWS Serverless Quarkus HTTP - lambda-http-localhost-1.0.0-SNAPSHOT
Resources:
LambdaHttpLocalhost:
Type: AWS::Serverless::Function
Properties:
Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
Runtime: java21
CodeUri: function.zip
MemorySize: 512
Timeout: 15That Handler value is the Quarkus bridge. Do not change it unless you are deliberately replacing the runtime path the extension generated for you.
The native SAM template switches runtime and adds one environment variable:
Runtime: provided.al2023
Environment:
Variables:
DISABLE_SIGNAL_HANDLERS: trueThat matches the current Quarkus AWS Lambda HTTP guide, which calls out both the Java runtime handler and the native runtime signal-handler setting.
Run the packaged Lambda with SAM CLI
The mock event server is the fast inner loop. sam local start-api is the heavier proof step that shows the packaged artifact running behind a local Lambda plus API Gateway simulation instead of the Quarkus dev loop.
For this section you need two extra tools:
AWS SAM CLI installed
A Docker-compatible container backend (Podman)
The current Quarkus AWS Lambda HTTP guide documents this flow as Docker-backed. In my local run for this article, I used SAM CLI 1.161.1 on macOS with Podman 5.8.2 because the machine already exposed a Docker-compatible socket at /var/run/docker.sock. Podman’s own installation docs say that macOS Podman can listen for Docker API clients, which is the part SAM needs.
If you are on Podman instead of Docker Desktop, check these two things before you blame SAM:
podman machine list
ls -l /var/run/docker.sockOn my machine, that socket was a symlink to the Podman machine socket, and SAM worked without extra flags.
Build the Lambda artifacts first:
./mvnw package -DskipTestsThen start the local API Gateway plus Lambda simulation from the generated SAM template:
sam local start-api --template target/sam.jvm.yamlSAM prints the mounted endpoint and then waits on port 3000. In my run, the first invoke pulled the Java 21 Lambda emulation image and started the Quarkus function inside that container.
When SAM is ready, call the same route through the local API endpoint on port 3000:
curl -s 'http://127.0.0.1:3000/quotes/lisbon?speed=express&weightGrams=900' \
-H 'X-Customer-Tier: gold' | jqYou should see the same business response shape as in dev mode:
{
"destination": "LISBON",
"speed": "express",
"customerTier": "gold",
"weightGrams": 900,
"quotedCents": 990,
"estimatedBusinessDays": 2,
"fulfillmentRegion": "eu-central",
"gatewayRequestId": "mock-event",
"stage": "$default"
}This response can be locally verified against the packaged Lambda. In my run, the gatewayRequestId value was generated by SAM and looked like this:
{
"destination": "LISBON",
"speed": "express",
"customerTier": "gold",
"weightGrams": 900,
"quotedCents": 990,
"estimatedBusinessDays": 2,
"fulfillmentRegion": "eu-central",
"gatewayRequestId": "4693c4af-047d-4f06-b58a-81194d4cbb16",
"stage": "$default"
}One small Podman-specific wrinkle showed up in the SAM logs:
Unknown 404 - Unable to check if base image is current.
Possible incompatible Docker engine clone employed.That warning did not block execution. The request still returned 200 OK. It is a compatibility warning about image freshness checks, not proof that the Lambda failed to run.
At that point the tutorial shows two different local truths:
Quarkus dev mode with the mock event server is the fast coding loop
SAM CLI is the packaging-level check that the generated Lambda service still answers correctly
One replaces friction during development. The other reduces hand-waving before you get to AWS.
A few honest boundaries
This adapter is useful, but I would keep a few things in mind:
The current Quarkus AWS Lambda HTTP guide still marks this extension as preview.
The mock event server is very good for API Gateway event shape, header behavior, and local code flow. It is not a full substitute for IAM, Cognito, networking, and deployment validation in a real AWS account.
Runtime alignment matters. If your generated SAM template says
java21, do not quietly ship Java 25 bytecode into it.Security mapping is off by default. If you want Quarkus to translate API Gateway security metadata into a principal, enable
quarkus.lambda-http.enable-security=trueand test with richer raw events.
None of that weakens the local development story. It just keeps the promise honest.
Close the loop
We started with the usual serverless complaint: the local loop gets worse the moment Lambda enters the room. With Quarkus AWS Lambda HTTP, that does not have to be true.
We built a normal Quarkus REST app, ran it through localhost:8080, injected API Gateway V2 metadata into the resource with @Context, pushed a raw event into /_lambda_, tested both paths, and packaged the app into the files AWS tooling expects. The useful mental model is simple: during development, localhost is not just your HTTP server anymore. With the mock event server in front, it is already pretending to be API Gateway and Lambda.


