Next Generation Node.js server Development Tool


By: Axe 2021-08-20

TL;DR

Use the awesome vite plugin I made to enable rapid node server development experiences! Star it if you like it!

Motivation

I'm recently working on a relatively large nestjs backend project. The slow dev server drives me crazy! Every time code is updated, it takes few seconds to restart the dev server. I tried replacing the nest CLI with webpack hope the webpack HMR could help with the recompiling speed. Although the HMR works just fine, after the module is updated, it still restarts the dev server to make the updated module take effect. That makes the time saved from HMR all wasted, cause the server restarting is quite time-consuming since it needs to kill the current process and respawn a new one. What if the server doesn't need to restart at all after a module is updated? What if those new amazing frontend tools also work for backend development? After few days of research/work, I made a plugin for Vitejs to allow me to run my node server with vite!

Stone Age Backend toolings

Before getting to know more about this amazing plugin. Let's talk about the existing dev tools for backend development nowadays. And I will explain why none of them is ideal.

nodemon

The idea behind this tool is very simple:

Monitor for any changes in your node.js application and automatically restart the server

It's good for writing some small pure javascript node server that boot time is negligible. On the other hand, that means slow reload time for large projects and lacking of typescript support.

ts-node

This is one of the most popular tools for run typescript files directly. However, the tool itself won't handle server reload for you. You have to use a tool like nodemon to help you to reload the server. That means it shares the disadvantages that came with nodemon.

nodejs registers/ esm-loader

A lot of people use various tools built on top of nodejs require hook(or the newer loaders for ESM). This is actually a very clever approach to make nodejs natively support to run typescript code. Also to implement an HMR system on top of this is actually quite easy. All we need to do is cache all the transformed code in cache and invalidate the cache after a module updated. Then next time when other module require/import the module, the transform will re-compile the updated the module. It's kinda JIT compiling, which performs much better than compile everything that starts from entry all over again. I actually tried to write my own ESM loader with ESBuild/SWC as typescript compiler and an in-memory cache to make this POC works. But when its almost halfway done, I realized I'm implementing something already exist in Vitejs source code! I'm reinventing the wheels here!

What's Vite anyway?

From Vite github description:

Vite (French word for "fast", pronounced /vit/) is a new breed of frontend build tool that significantly improves the frontend development experience. It consists of two major parts:

A dev server that serves your source files over native ES modules, with rich built-in features and astonishingly fast Hot Module Replacement (HMR).

A build command that bundles your code with Rollup, pre-configured to output highly optimized static assets for production.

In addition, Vite is highly extensible via its Plugin API and JavaScript API with full typing support.

I know, it's made for frontend development originally. But we some tweaking, we can use it for backend I promise! Some keywords we looking at the description are fast HMR, Dev Server, extensible. Allow me to explain how we can leverage these features for node server development.

Ultra-fast HMR

How? Vite internally builds a module graph to keep tracking all the dependencies and caching all transformed source code at the start. This enables the JIT like HMR like what we talked about early.

Dev Server

Vite internally uses connect as an HTTP server and the server is customizable and extensible with Vite plugin/javascript API. These means we could pass down the raw nodejs HTTP request from vite dev server to our own app. All those handy configs like local https are also very helpful for local dev.

Highly Extensible

Thanks for the excellent API design. We can almost customize everything in vite. By default, Vite is using esbuild to transform typescript, want to use swc to handle some decorator feature that esbuild not support yet? No problem, just use the swc plugin to handle typescript transformation, ez!pz!

Last to mention but not the least

Vite is created by Evan You, the same author of Vue.js. Shamelessly to admit I'm his big fan. Evan can always find a very unique way to solve some common problems.

Vite Node Plugin

Finally, let me present my baby plugin. The repo is at axe-me/vite-plugin-node. Please kindly give it a star if you like it after trying it out.

How it works?

Vite by design has a middlewareMode which allow us to use vite programmatically inside other modules. It's originally made for SSR web app. So that for each request, vite can load the renderer to render the latest changes you made to your app. This plugin leverage this feature to load and execute your server app entry with the incoming HTTP request.

You may ask isn't super slow since it re-compiles/reload the entire app from the entry? The answer is NO because vite is smart. Vite use the builtin module graph as a cache layer, the graph is built up at the first time your app load. After that, when you update one file, vite will only invalidate itself and its parents' modules, so that for the next request, only those invalidated modules need to be re-compiled which is super fast thanks to ESBuild and SWC.

How to use it?

You can read the readme from the repo since that is the only source of truth document. Or just keep reading this post to get some idea first:

  1. Install vite and this plugin with your favorite package manager, here use npm as example:

    npm install vite vite-plugin-node -D
    
  2. Create a vite.config.ts file in your project root to config vite to actually use this plugin:

    import { defineConfig } from 'vite';
    import { VitePluginNode } from 'vite-plugin-node';
    
    export default defineConfig({
      // ...vite configures
      server: { // vite server configs, for details see [vite doc](https://vitejs.dev/config/#server-host)
        port: 3000
      },
      plugins: [
        ...VitePluginNode({
          // Nodejs native Request adapter
          // currently this plugin support 'express', 'nest', 'koa' and 'fastify' out of box,
          // you can also pass a function if you are using other frameworks, see Custom Adapter section
          adapter: 'express', 
    
          // tell the plugin where is your project entry
          appPath: './app.ts',
    
          // Optional, default: 'viteNodeApp' 
          // the name of named export of you app from the appPath file
          exportName: 'viteNodeApp',
    
          // Optional, default: 'esbuild'
          // The TypeScript compiler you want to use
          // by default this plugin is using vite default ts compiler which is esbuild
          // 'swc' compiler is supported to use as well for frameworks
          // like Nestjs (esbuild dont support 'emitDecoratorMetadata' yet)
          tsCompiler: 'esbuild',
        })
      ]
    }
    
  3. Update your server entry to export your app named viteNodeApp or the name you configured.

    ExpressJs

    const app = express();
    
    // your beautiful code...
    
    if (process.env.NODE_ENV === 'production') {
      app.listen(3000)
    }
    
    export const viteNodeApp = app;
    

    KoaJs

    import Koa from 'koa';
    
    const app = new Koa();
    
    // your beautiful code...
    
    if (process.env.NODE_ENV === 'production') {
        app.listen(3000)
    }
    
    export const viteNodeApp = app;
    

    Fastify

    import fastify from 'fastify';
    
    const app = fastify();
    
    // your beautiful code...
    
    if (process.env.NODE_ENV === 'production') {
        app.listen(3000)
    }
    
    export const viteNodeApp = app;
    

    if the app created by an async factory function you can just export the promise.

    import fastify from 'fastify';
    
    const app = async (options) => {
      const app = fastify(options);
      // app logics...
      return app
    }
    
    // note here we need to run the function to get the promise.
    export const viteNodeApp = app(options);
    

    NestJs

    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    
    if (process.env.NODE_ENV === 'production') {
      async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        await app.listen(3000);
      }
    
      bootstrap();
    }
    
    export const viteNodeApp = NestFactory.create(AppModule); // this returns a Promise, which is ok, this plugin can handle it
    
  4. Add a npm script to run the dev server:

    "scripts": {
      "dev": "vite"
    },
    
  5. Run the script! npm run dev

Custom Adapter

If your favorite framework not supported yet, you can either create an issue to request it or use the adapter option to tell the plugin how to pass down the request to your app. You can take a look how the supported frameworks implementations from the ./src/server folder.
Example:

import { defineConfig } from 'vite';
import { VitePluginNode } from 'vite-plugin-node';

export default defineConfig({
  plugins: [
    ...VitePluginNode({
      adapter: function(app, req, res) {
        app(res, res)
      },
      appPath: './app.ts'
    })
  ]
})

Examples

There is an examples folder in the repo that contains different use cases.

The End

This plugin helps me to be much more productive, only because I don't need to wait few seconds for server reload so that my mind can keep rolling without any breaks in the middle. Hopefully, more people can start to use vite with my plugin for backend server development. My ultimate goal is replacing nodemon/ts-node etc stone age tools.
Happy Coding!

Happy on coach