Lightning-Fast Deployment and Runtime: "Nitro" - Recommended Runtime for AWS Lambda Node.js Web Servers
Usually, when building web servers on AWS Lambda, I use Rust, but there was a dependency package that required Node.js. This led me to seriously reconsider which framework would be best, and I ended up re-evaluating Nitro, which I had previously settled on as my solution.
What is Nitro?#
Nitro is a web framework that can build for multiple deployment targets without code changes.
Nitro takes a significantly different approach compared to other well-known web frameworks.
Nitro is Compiler Macro-Based#
Typical web frameworks often involve creating a server instance, registering routes to that instance, and then starting the server.
However, Nitro uses compiler macros to define routes, and users don't directly write code to start the server.
For example, you can simply write code like this and immediately build for various deployment targets:
export default defineEventHandler((event) => {
return { message: "Hello, world!" };
});
And by just setting the preset
in the configuration file, you can create build artifacts optimized for each platform.
For instance, if you want to deploy to AWS Lambda, you only need to configure it like this:
export default defineNitroConfig({
srcDir: "server",
preset: "aws-lambda"
});
When deploying to VPS, containers, or on-premises servers, you can make it run on Node.js like this:
export default defineNitroConfig({
srcDir: "server",
preset: "node_server"
});
In other words, Nitro allows you to write code once and generate artifacts adapted to various providers without fear of vendor lock-in.
Directory Structure Becomes Route Path Structure#
In Nitro, by default, the directory structure under server/routes/
directly becomes the route structure.
For example:
server/routes/index.ts
→ANY /
server/routes/greet.get.ts
→GET /greet
server/routes/auth/signup.post.ts
→POST /auth/signup
Lightweight & Optimized#
When building web servers for enterprise use, NestJS + Fastify is a safe choice, but NestJS has the drawback of being very slow to start in serverless environments. This is the inevitable consequence of using script languages with high levels of abstraction, but NestJS has intensive abstraction not just on the user side but also on the package side, so it takes time for JIT compilation to become effective.
In contrast, Nitro doesn't have Express or Fastify in between like NestJS does, allowing optimization for each platform. For Lambda functions as a target, no transport is necessary, making optimization easier.
Super Easy - Deploying to AWS Lambda#
Following these steps, you can deploy a web server to AWS Lambda right away. Yes, with Nitro.
Standard setup guides are also available at the link below.
Project Initialization#
There's a Nitro template available, so initialize with giget. Verify that a directory has been created with the project name you entered.
npx giget@latest nitro <project-name> --install
Starting the Server Locally#
Navigate to the created directory.
cd <project-name>
Modify server/routes/index.ts
as follows:
export default defineEventHandler((event) => {
return { message: "Hello, world!" };
});
Start the development server via npm script.
npm run dev
Access http://localhost:3000 to verify that responses are being returned properly.
Building for AWS Lambda#
Modify the configuration file to build for AWS Lambda.
export default defineNitroConfig({
srcDir: "server",
preset: "aws-lambda"
});
Build via npm script.
npm run build
Archive into a zip file for deployment. This will create .output/lambda.zip
.
cd .output/server
zip -r ../lambda.zip .
Deploying to AWS Lambda#
Create a Lambda function with the following settings and deploy the zip archive:
- Runtime:
Node.js
(choose the version appropriate for your environment) - Handler:
index.handler
And just like that, your web server is complete! For a simple test, create Function URLs and try it out. Also, since Function URLs and API Gateway events are the same, you can use this as an API Gateway integration as well.
Bonus: Implementing Response Streaming#
AWS Lambda supports HTTP response streaming in Node.js and custom runtimes. This is particularly important in the current LLM boom, where outputs are often extracted bit by bit through asynchronous iteration.
Defining a Route that Returns a Stream#
Define a route at GET /stream that returns a stream. Examples can be found in the h3 documentation.
Here's a working code snippet you can copy and paste. Since LLM responses are often returned as asynchronous iterators, we're using a generator function to make it more understandable.
const sleep = async (duration: number) =>
new Promise((resolve) => setTimeout(resolve, duration));
async function* streaming(): AsyncGenerator<string> {
yield "<ul>";
let counter = 0;
while (counter < 20) {
await sleep(100);
yield `<li>${Math.random()}</li>`;
counter++;
}
yield "</ul>";
}
export default defineEventHandler((event) => {
setResponseHeader(event, "Content-Type", "text/html");
setResponseHeader(event, "Cache-Control", "no-cache");
setResponseHeader(event, "Transfer-Encoding", "chunked");
const stream = new ReadableStream({
async start(controller) {
for await (const value of streaming()) {
controller.enqueue(value);
}
controller.close();
},
});
return sendStream(event, stream);
});
Local Testing#
Verify that the route definition is correct. Start the development server and access http://localhost:3000/stream
. If responses come back at regular intervals, you're good to go.
npm run dev
Or you can check with curl
:
curl --no-buffer http://localhost:3000/stream
Deployment and Function URLs Considerations#
You can deploy in the same way as before. However, when setting up Function URLs, be sure to set the "Invoke mode" to "RESPONSE_STREAM
".