Skip to content

Nundy/nuxt-elysia

 
 

Repository files navigation

en_flag English | cn_flag 简体中文

Use Elysia app in Nuxt (Multi-Service, Single Service)

fork from >> tkesgar/nuxt-elysia

Features

  • Directly mount Elysia in Nuxt
    • Simplify development setup (you do not have to run Elysia app server separately)
    • Simplify deployment (deploy only one server)
    • Support multi-service (fully unleash its performance)
  • Eden Treaty integration
    • Full Eden Treaty features (end-to-end type safety, lightweight size)
    • Isomorphic client: Eden Treaty works in both server-side and client- side without additional configuration
    • Pass headers sent by browser to Elysia app (the list of headers is configurable)
  • Works in both Node.js and Bun
    • Run in Bun for maximum performance
    • Run in Node.js for better compatibility with some packages (while waiting for full Node.js compatibility from Bun)

Setup

Requirements: Node v20+ or Bun v1

Install the package:

# Bun
bun add @whispering/nuxt-elysia -D
bun add elysia @elysiajs/eden

# NPM
npm install @whispering/nuxt-elysia --save-dev
npm install elysia @elysiajs/eden

See Running in Bun below on how to run Nuxt applications in Bun instead of Node.js.

nuxt-elysia declares elysia and @elysiajs/eden as peer dependency, which will be automatically installed by most package managers (Bun, NPM, PNPM). However, by explicitly declaring the peer dependency you will be able to control the specific Elysia and Eden Treaty version to use.

nuxt-elysia should be only installed as devDependency, since it is only necessary during development and build. It is not needed in production environment.

Add to modules list in nuxt.config.ts:

export default defineNuxtConfig({
  modules: [
    // ...
    "@whispering/nuxt-elysia",
  ],
});

Create api.ts in the project root:

export default () =>
  new Elysia().get("/hello", () => ({ message: "Hello world!" }));

Use in Vue app:

<template>
  <div>
    <p>{{ helloMessage }}</p>
  </div>
</template>
<script setup lang="ts">
const { $api } = useNuxtApp();

const { data: helloMessage } = await useAsyncData(async () => {
  const { data, error } = await $api.hello.get();

  // Due to Eden Treaty's type safety, you need to handle if `error` is truthy:
  // https://elysiajs.com/eden/treaty/response.html#response
  //
  // Throwing an error here will make it available in the `useAsyncData` error.
  //
  if (error) {
    throw new Error("Failed to call API");
  }

  return data.message;
});
</script>

Module options

export interface ModuleOptions {
  /**
   * Specifies the module that exports the Elysia app factory function.
   *
   * The default value `~~/api` is a Nuxt default alias for `/api` path in
   * the Nuxt project root. This alias may resolve to `<root>/api.ts` or
   * `<root>/api/index.ts`.
   *
   * Default: `~~/api`
   */
  module: string;
  /**
   * Specifies the path to mount the Elysia app.
   * 
   * Configuration Options for Elysia API Services Supports two modes:
   * 1. ​​String Format (Single Service), Example: '_api', Description: Mounts Elysia onto the Nitro server.
   * 2. Object Format (Multi-Service, Standalone)​​, Example: { host, port, prefix, isStart }, Description: Runs Elysia as a standalone service to fully unleash its performance.
   *
   * Set to empty string (`''`) to disable mounting the Elysia app.
   *
   * Default: `/_api`
   */
  path: string | PathOptions;
  /**
   * Whether to enable Eden Treaty plugin.
   *
   * Default: `true`
   */
  treaty: boolean;
  /**
   * When mounting the Elysia app in Bun, Elysia handler that returns a string
   * will not have any `Content-Type` header:
   *
   * ```ts
   * const app = new Elysia()
   *   .get('/plaintext', () => 'Hello world!)
   * ```
   *
   * This option adds a transform to add `Content-Type: text/plain`.
   *
   * Default: `true`
   */
  fixBunPlainTextResponse: boolean;
  /**
   * Provides the list of request headers to be sent to the Elysia app on
   * server-side requests.
   *
   * The default value is `['Cookie']`, which will pass all cookies sent by
   * the browser to Elysia app. Set to `false` to disable passing any headers.
   *
   * Default: `['Cookie']`
   */
  treatyRequestHeaders: string[] | false;
}

/**
 * ​​Parameters for Standalone API Service:
 */
export interface PathOptions {
  /**
   * API service host address (e.g., 'http://localhost').
   */
  host: string;
  /**
   * API service port number (e.g., 4000).
   */
  port: number;
  /**
   * API path prefix (e.g., '_api').
   */
  prefix: string;
  /**
   * Whether to automatically start the standalone API service (Boolean value).
   */
  isStart?: boolean;
}

Notes

Known quirks

Because nuxt-elysia mounts Elysia as a handler for H3 application instead of directly handling the HTTP request, there may be several quirks that we need to fix with additional wrappers and transforms. You can check server-plugin.ts generated from server-plugin.template for the list of currently implemented workarounds.

Our goal is to ensure the same results between mounting the Elysia app and running the Elysia app as separate server (directly in Bun or running in Node.js via @elysiajs/node adapter).

module option

You can use any aliases from Nuxt in module option.

The default value for module is ~~/api, which is a Nuxt default alias for <root>/api path in the Nuxt project root. The path may resolve to <root>/api.ts or <root>/api/index.ts.

Other paths you can use:

export default defineNuxtConfig({
  nuxtElysia: {
    // Custom alias
    module: "#api",
    // Module in other package
    module: "@my-org/my-package",
    // Absolute path
    module: "/absolute/path/to/module",
    // Generated module (from other Nuxt module)
    module: "~~/.nuxt/my-generated-module",
  },
});

Only mount in development

To only mount the app in development, use import.meta.dev (which will be false when building to production):

export default defineNuxtConfig({
  nuxtElysia: {
    module: "@my-org/my-server-app",
    path: import.meta.dev ? "/_api" : "",
  },
});
nuxtElysia: {
  path: process.env.NODE_ENV === 'production'
    ? {
        host: 'http://localhost',
        port: 4000,
        prefix: '/_api',
        isStart: true,
      }
    : '/_api',
}

This is useful if you only want to mount the Elysia app in development setup and uses a reverse proxy to serve the app in separate instance. For example, using Nginx:

location /_api {
  proxy_pass http://my-api-service;
}

location / {
  proxy_pass http://my-nuxt-app;
}

Running the application in Bun

The Elysia app is mounted as request handler for the Nitro application stack, so you can use Nuxt Elysia without Bun.

If you want to use Bun-specific APIs (Bun.*), you will need to run Nuxt using Bun. Bun respects node shebang, meaning that nuxt dev actually uses Node.js (if both Node.js and Bun are available). Therefore, you need to add --bun flag to override this behavior:

{
  "scripts": {
    "dev": "bun --bun dev",
    "build": "bun --bun build",
    "preview": "bun --bun preview"
  }
}

Note that you only need to do this if you have both Node.js and Bun installed.

If you do this you will also need to use Nitro Bun preset to build the app. This is because the default node-server preset will fail to bundle Elysia, since Elysia has Bun-specific exports that will not be handled properly by the default preset:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "bun": "./dist/bun/index.js",
      "import": "./dist/index.mjs",
      "require": "./dist/cjs/index.js"
    }
  }
}

Furthermore, you will also need to include the root node_modules in your deployment, as opposed to only .output directory. This is because the Bun-specific packages will be read from the root directory:

<root>
├── .output
├── node_modules
├── package.json
└── bun.lock

If you use Docker to containerize your app, you can use this Dockerfile as reference:

# Use Bun base image
FROM oven/bun:1-slim

# Set working directory
WORKDIR /app

# Set NODE_ENV=production (prevents development-specific logs from some
# packages such as vue-router)
ENV NODE_ENV=production

# Copy .output directory generated by Nuxt
# Make sure to run `nuxt build` before building the container image
COPY .output .output

# Copy package.json and bun.lock, then run `bun install --production`
# to install only production dependencies.
COPY package.json bun.lock ./
RUN bun install --production

# Set working user
USER bun

# Expose port 3000 (default Nitro port)
EXPOSE 3000

# Set image entrypoint (run the generated server module using Bun)
ENTRYPOINT [ "bun", "./.output/server/index.mjs" ]

Benchmark

The benchmark Nuxt app is available in test/fixtures/benchmark.

We run the tests using bombardier on the following machine:

                          ./+o+-       tkesgar@tkesgar-ideapad
                  yyyyy- -yyyyyy+      OS: Ubuntu 24.04 noble(on the Windows Subsystem for Linux)
               ://+//////-yyyyyyo      Kernel: x86_64 Linux 5.15.167.4-microsoft-standard-WSL2
           .++ .:/++++++/-.+sss/`      Uptime: 1h 16m
         .:++o:  /++++++++/:--:/-      Packages: 581
        o:+o+:++.`..```.-/oo+++++/     Shell: bash 5.2.21
       .:+o:+o/.          `+sssoo+/    Resolution: No X Server
  .++/+:+oo+o:`             /sssooo.   WM: Not Found
 /+++//+:`oo+o               /::--:.   GTK Theme: Adwaita [GTK3]
 \+/+o+++`o++o               ++////.   Disk: 424G / 1.7T (27%)
  .++.o+++oo+:`             /dddhhh.   CPU: 13th Gen Intel Core i5-1335U @ 12x 2.496GHz
       .+.o+oo:.          `oddhhhh+    RAM: 4426MiB / 7807MiB
        \+.++o+o``-````.:ohdhhhhh+
         `:o+++ `ohhhhhhhhyo++os:
           .o:`.syhhhhhhh/.oo++o`
               /osyyyyyyo++ooo+++/
                   ````` +oo+++o\:
                          `oo++.

Result:

name framework runtime avg reqs/s avg latency throughput
api-json elysia bun 14704.61 8.50 3.27
api-json elysia node 7003.07 17.92 1.88
api-json h3 bun 14084.20 8.87 2.93
api-json h3 node 15987.32 7.82 4.04
api-text elysia bun 13556.04 9.22 2.57
api-text elysia node 8009.46 15.59 2.02
api-text h3 bun 17536.14 7.13 3.06
api-text h3 node 15498.33 8.06 3.40
nuxt-render elysia bun 1173.03 107.59 1.90
nuxt-render elysia node 665.51 186.77 1.12
nuxt-render h3 bun 1019.14 123.27 1.72
nuxt-render h3 node 929.24 133.59 1.63

Remarks:

  • Prefer running the Nuxt app in Bun instead of Node.js if possible.
  • There is no performance benefit from using Elysia instead of H3 in Node.js; in fact, there is noticeable slowdown due to the native Response overhead (H3 directly works with the native HTTP payload).
  • There is no noticeable performance issue with the server-side API client (Elysia: Eden Treaty, Nitro: mock ofetch).

Contributing

Requirements:

  • Bun
  • Node.js

Development steps:

  1. Clone this repository
  2. Install dependencies: bun install
  3. Stub modules for development: bun dev:prepare
  4. Run playground in development mode: bun dev
  5. Run lint: bun lint
  6. Run typecheck: bun test:types
  7. Testing:
  • Testing in Node.js:
    1. Run bun dev in separate terminal
    2. Run bun test
  • Testing in Bun:
    1. Run bun dev:bun in separate terminal
    2. Run bun test
  • Test building output:
    • Node.js: bun dev:build
    • Bun: bun dev:build:bun
  • Running built output:
    • Node.js: bun dev:start
    • Bun: bun dev:start:bun

License

MIT License

About

Mount Elysia app in Nuxt

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 58.0%
  • EJS 31.4%
  • Vue 8.4%
  • JavaScript 2.2%