LanguageEnglish

Lightning-Fast Deployment and Runtime: "Nitro" - Recommended Runtime for AWS Lambda Node.js Web Servers

2025-07-18
2025-07-18

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:

typescript
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:

typescript
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:

typescript
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.tsANY /
  • server/routes/greet.get.tsGET /greet
  • server/routes/auth/signup.post.tsPOST /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 favicongiget. Verify that a directory has been created with the project name you entered.

bash
npx giget@latest nitro <project-name> --install

Starting the Server Locally#

Navigate to the created directory.

bash
cd <project-name>

Modify server/routes/index.ts as follows:

typescript
export default defineEventHandler((event) => {
  return { message: "Hello, world!" };
});

Start the development server via npm script.

bash
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.

typescript
export default defineNitroConfig({
  srcDir: "server",
  preset: "aws-lambda"
});

Build via npm script.

bash
npm run build

Archive into a zip file for deployment. This will create .output/lambda.zip.

bash
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 faviconh3 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.

typescript
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.

bash
npm run dev

Or you can check with curl:

bash
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".

Ikuma Yamashita
Cloud engineer. Works on infrastructure-related tasks professionally, but has been spotted dedicating private time exclusively to systems programming. Shows a preference for using Rust. Enjoys illustration as a hobby.