Blazing-fast deploys and runtime: Nitro, the recommended Node.js web server runtime for AWS Lambda
Usually, when building web servers on AWS Lambda, I use Rust, but I had a dependency package that required Node.js. At this point, I seriously reconsidered which framework would be best and ended up reevaluating Nitro, which I had previously concluded was the answer.
What is Nitro?#
Nitro is a web framework that can build for many deployment targets without changing code.
Nitro takes a quite different approach compared to other well-known web frameworks.
Nitro is Compiler Macro-Based#
Typical web frameworks often create a server instance, register routes to that instance, and start the server.
However, Nitro uses compiler macros to define routes, and users don't directly write the code to start the server.
For example, by simply writing code like the following, you can immediately build for various deployment targets.
export default defineEventHandler((event) => {
return { message: "Hello, world!" };
});And by simply setting the preset in the configuration file, you get a build artifact optimized for each platform.
For example, if you want to deploy to AWS Lambda, you only need to configure it as follows.
export default defineNitroConfig({
srcDir: "server",
preset: "aws-lambda"
});When deploying to VPS, containers, or on-premises servers, you just need to run it with Node.js.
export default defineNitroConfig({
srcDir: "server",
preset: "node_server"
});In other words, once you write code with Nitro, you can generate artifacts that fit various providers without fear of vendor lock-in.
Directory Structure Becomes Route Path Structure#
Nitro has the characteristic that, by default, the directory structure under server/routes/ directly becomes the route structure.
For example, it works as follows:
server/routes/index.ts→ANY /server/routes/greet.get.ts→GET /greetserver/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. While it's inevitable that high-level abstraction in scripting languages leads to slowness, NestJS has extensive abstraction not only on the user side but also on the package side, so it takes time until JIT compilation kicks in.
In contrast, Nitro doesn't have Express or Fastify in between like NestJS does, allowing optimization for each platform. If Lambda functions are the target, no transport layer is needed, making optimization easy.
Super Easy - Deploy to AWS Lambda#
If you follow the steps below, you can really deploy a web server to AWS Lambda right away. Yes, with Nitro.
Note that the standard setup guide is also available at the following link.
Initialize the Project#
Since there's a Nitro template, initialize it with giget. Confirm that a directory is created with the project name you entered.
npx giget@latest nitro <project-name> --installStart the Local Server#
Navigate to the created directory.
cd <project-name>Rewrite server/routes/index.ts as follows.
export default defineEventHandler((event) => {
return { message: "Hello, world!" };
});Start the development server with the npm script.
npm run devAccess http://localhost:3000 and confirm that the response is returned properly.
Build for AWS Lambda#
Modify the configuration file so you can build for AWS Lambda.
export default defineNitroConfig({
srcDir: "server",
preset: "aws-lambda"
});Build via the npm script.
npm run buildArchive it as a zip for deployment. This creates .output/lambda.zip.
cd .output/server
zip -r ../lambda.zip .Deploy to AWS Lambda#
Create a Lambda function with the following settings and deploy the zip archive.
- Runtime:
Node.js(adjust the version to your environment) - Handler:
index.handler
That's it—your web server is now complete! To test it quickly, create a Function URL and try it out. Also, since Function URLs and API Gateway events are the same, you can use this as-is for API Gateway integration.
Bonus: Implementing Response Streaming#
AWS Lambda supports HTTP response streaming in Node.js runtime and custom runtimes. Especially with the recent LLM boom, where outputs are often extracted incrementally through asynchronous iteration, response streaming is very important.
Define a Route that Returns a Stream#
Define a route at GET /stream that returns a stream. There's an example in the h3 documentation.
Below is code that works with copy-paste. Since LLM responses are often returned as async iterators, I've included a generator function to make it clearer.
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);
});
Test Locally#
Verify that the route definition is correct. Start the development server and access http://localhost:3000/stream—if responses are coming back at regular intervals, you're good.
npm run devAlternatively, you can verify with curl.
curl --no-buffer http://localhost:3000/streamDeployment and Function URLs Note#
You can deploy in the same way. However, when configuring Function URLs, make sure to set "Invoke mode" to "RESPONSE_STREAM".