diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 27138349..bd8838bc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -238,6 +238,17 @@ updates: security: applies-to: security-updates patterns: ["*"] + - package-ecosystem: npm + directories: ["/examples/hono-zip/app"] + schedule: + interval: weekly + open-pull-requests-limit: 0 + commit-message: + prefix: chore + groups: + security: + applies-to: security-updates + patterns: ["*"] - package-ecosystem: npm directories: ["/examples/nextjs/app"] schedule: diff --git a/.github/example-matrix.json b/.github/example-matrix.json index 6c4f8d97..4101ab03 100644 --- a/.github/example-matrix.json +++ b/.github/example-matrix.json @@ -16,6 +16,7 @@ { "name": "fasthtml-zip", "path": "/", "expect_body": "Hello World", "port": "8000" }, { "name": "flask-zip", "path": "/", "expect_body": "message", "port": "8000" }, { "name": "gin-zip", "path": "/", "expect_body": "message", "port": "8000" }, + { "name": "hono-zip", "path": "/", "expect_body": "Hello from Hono", "port": "8000" }, { "name": "remix-zip", "path": "/", "expect_body": "Welcome to", "port": "8000" }, { "name": "springboot-zip", "path": "/v1/", "expect_body": "Hello, world!", "port": "8000" } ], diff --git a/README.md b/README.md index 67c2f2fb..628630b4 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,7 @@ See the [FastAPI with SnapStart example](examples/fastapi-snapstart-zip) for a c - [Remix](examples/remix/) - [Remix in Zip](examples/remix-zip/) - [Sveltekit SSR Zip](examples/sveltekit-ssr-zip/) +- [Hono in Zip](examples/hono-zip) - [Datadog](examples/datadog) - [Datadog in Zip](examples/datadog-zip) diff --git a/docs/guide/src/examples/overview.md b/docs/guide/src/examples/overview.md index b050c732..acdc0002 100644 --- a/docs/guide/src/examples/overview.md +++ b/docs/guide/src/examples/overview.md @@ -37,6 +37,7 @@ The repository includes working examples for many popular web frameworks, packag | [Remix](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/remix) | Docker | No | | [Remix in Zip](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/remix-zip) | Zip | No | | [SvelteKit SSR Zip](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/sveltekit-ssr-zip) | Zip | No | +| [Hono in Zip](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/hono-zip) | Zip | No | | [Bun GraphQL](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/bun-graphql-zip) | Zip | No | | [Bun GraphQL Streaming](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/bun-graphql-streaming-zip) | Zip | Yes | diff --git a/examples/hono-zip/.gitignore b/examples/hono-zip/.gitignore new file mode 100644 index 00000000..3ef28a96 --- /dev/null +++ b/examples/hono-zip/.gitignore @@ -0,0 +1,2 @@ +node_modules +.aws-sam diff --git a/examples/hono-zip/README.md b/examples/hono-zip/README.md new file mode 100644 index 00000000..b69e2e4c --- /dev/null +++ b/examples/hono-zip/README.md @@ -0,0 +1,80 @@ +# hono-zip + +This example shows how to use Lambda Web Adapter to run a [Hono](https://hono.dev) application on the managed Node.js runtime. + +The application is a Node.js HTTP server. It uses [@hono/node-server](https://github.com/honojs/node-server) to start the server, and it has no Lambda-specific code. Thus you can run the same `index.js` file on Lambda, in a container, or on your computer. + +### How does it work? + +Add the Lambda Web Adapter layer to the function and configure the wrapper script. + +1. Attach the Lambda Web Adapter layer to the function. The layer contains the Lambda Web Adapter binary and a wrapper script. + 1. x86_64: `arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:30` + 2. arm64: `arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerArm64:30` +2. Set the Lambda environment variable `AWS_LAMBDA_EXEC_WRAPPER` to `/opt/bootstrap`. This is the wrapper script in the layer. +3. Set the function handler to the startup command `run.sh`. The wrapper script runs this command to start the application. + +The application listens on the port that the `PORT` environment variable gives. Lambda Web Adapter uses the same variable to find the application. + +For more information about wrapper scripts, refer to the [Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper). + +### Build and Deploy + +Run these commands to build the application and deploy it to Lambda. + +```bash +sam build +sam deploy --guided +``` + +When the deployment is complete, find the `HonoApi` output. Its value is the URL of the API Gateway endpoint. + +### Verify it works + +Send a request to the `HonoApi` URL. + +```bash +curl https://.execute-api..amazonaws.com/ +``` + +```json +{"message":"Hello from Hono!","requestId":"","functionName":""} +``` + +Lambda Web Adapter sends the request context from API Gateway and the Lambda context to the application. It puts them in the `x-amzn-request-context` and `x-amzn-lambda-context` headers. The application reads the request ID from the first header and the function name from the second header. + +The two contexts contain your AWS account ID. Thus the application does not return the full contexts. + +The application also has a route with a path parameter. + +```bash +curl https://.execute-api..amazonaws.com/hello/lambda +``` + +```json +{"message":"Hello, lambda!"} +``` + +### Run it locally + +You can run the application without Lambda. + +```bash +cd app +npm install +PORT=8000 npm start +``` + +Then send a request from a second terminal. + +```bash +curl http://localhost:8000/ +``` + +### Clean up + +Delete the stack when you do not need the example. + +```bash +sam delete +``` diff --git a/examples/hono-zip/app/index.js b/examples/hono-zip/app/index.js new file mode 100644 index 00000000..72c01264 --- /dev/null +++ b/examples/hono-zip/app/index.js @@ -0,0 +1,34 @@ +import { serve } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono() +const port = Number(process.env.PORT || 8080) + +// Lambda Web Adapter forwards the API Gateway request context and the Lambda +// context to the app as JSON strings in two HTTP headers. +const parseContext = (header) => (header ? JSON.parse(header) : null) + +app.get('/', (c) => { + const requestContext = parseContext(c.req.header('x-amzn-request-context')) + const lambdaContext = parseContext(c.req.header('x-amzn-lambda-context')) + + // Return two fields only. The full contexts contain the AWS account ID. + return c.json({ + message: 'Hello from Hono!', + requestId: requestContext?.requestId ?? null, + functionName: lambdaContext?.env_config?.function_name ?? null, + }) +}) + +app.get('/hello/:name', (c) => { + return c.json({ message: `Hello, ${c.req.param('name')}!` }) +}) + +const server = serve({ fetch: app.fetch, port }, (info) => { + console.log(`Hono app listening on port ${info.port}`) +}) + +// Lambda sends SIGTERM before it shuts down the execution environment. +process.on('SIGTERM', () => { + server.close(() => process.exit(0)) +}) diff --git a/examples/hono-zip/app/package-lock.json b/examples/hono-zip/app/package-lock.json new file mode 100644 index 00000000..efdcc681 --- /dev/null +++ b/examples/hono-zip/app/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "hono-zip", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hono-zip", + "version": "1.0.0", + "dependencies": { + "@hono/node-server": "^2.1.4", + "hono": "^4.13.13" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.4.tgz", + "integrity": "sha512-2g1qeHl4BduwDn6L8nkAvDrPUWPMlkQ//a5NhP/zIyaXKMjZNRV+cG9Csu+BiJFf6+68pqzjf4Hi5sJGKHuS1g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4 || ^5.0.0-0" + } + }, + "node_modules/hono": { + "version": "4.13.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", + "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + } + } +} diff --git a/examples/hono-zip/app/package.json b/examples/hono-zip/app/package.json new file mode 100644 index 00000000..ddf56307 --- /dev/null +++ b/examples/hono-zip/app/package.json @@ -0,0 +1,15 @@ +{ + "name": "hono-zip", + "version": "1.0.0", + "description": "Hono example for AWS Lambda Web Adapter", + "private": true, + "type": "module", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@hono/node-server": "^2.1.4", + "hono": "^4.13.13" + } +} diff --git a/examples/hono-zip/app/run.sh b/examples/hono-zip/app/run.sh new file mode 100755 index 00000000..dc62751d --- /dev/null +++ b/examples/hono-zip/app/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +exec node index.js diff --git a/examples/hono-zip/template.yaml b/examples/hono-zip/template.yaml new file mode 100644 index 00000000..63fb0926 --- /dev/null +++ b/examples/hono-zip/template.yaml @@ -0,0 +1,44 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + hono-zip + + Sample SAM Template for hono-zip + +# More info about Globals: https://github.com/aws/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 10 + +Resources: + HonoFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/aws/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: app/ + Handler: run.sh + Runtime: nodejs22.x + MemorySize: 512 + Architectures: + - x86_64 + Environment: + Variables: + AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap + # Listen on 8000 so the app doesn't collide with SAM's Runtime + # Interface Emulator on 8080 during local testing. + PORT: 8000 + Layers: + - !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:30 + Events: + HttpEvents: + Type: HttpApi + +Outputs: + HonoApi: + Description: "API Gateway endpoint URL for Hono function" + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/" + HonoFunction: + Description: "Hono Lambda Function ARN" + Value: !GetAtt HonoFunction.Arn + HonoFunctionIamRole: + Description: "Implicit IAM Role created for Hono function" + Value: !GetAtt HonoFunctionRole.Arn