npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

inertianest

v1.0.1

Published

A package for Inertia.js adapters for Nestjs with (Express and Fastify)

Readme

Inertianest

Inertia.js adapters for Express and Fastify in NestJS applications.

Installation

npm install inertianest

Setup

With Express

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
import { join } from 'path';
import { readFileSync } from 'fs';
import { Request, Response, NextFunction } from 'express';

interface ViteManifestEntry {
  file: string;
  src?: string;
  isEntry?: boolean;
  css?: string[];
  imports?: string[];
}

type ViteManifest = Record<string, ViteManifestEntry>;

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  
  // Setup view engine (example with ejs)
  app.useStaticAssets(join(__dirname, '..', 'public'));
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('ejs');

  // Load manifest.json ke locals supaya bisa dipakai di semua ejs
  const manifestPath = join(
    __dirname,
    '..',
    'public',
    '.vite',
    'manifest.json',
  );

  const manifest = JSON.parse(
    readFileSync(manifestPath, 'utf-8'),
  ) as ViteManifest;
  const entry = manifest['main.js'];

  app.use((req: Request, res: Response, next: NextFunction) => {
    res.locals.viteEntry = entry;
    next();
  });

  await app.listen(3000);
}

example home.module.ts

import { Module } from '@nestjs/common';
import { HomeController } from './home.controller';
import { InertiaModule } from 'inertianest';

@Module({
  imports: [
    InertiaModule.register({
      adapter: 'express',
      view: 'app', // Your base view file name
      version: '1.0',
    }),
  ],
  controllers: [HomeController],
})
export class HomeModule {}

With Fastify

import { NestFactory } from '@nestjs/core';
import { NestFastifyApplication, FastifyAdapter } from '@nestjs/platform-fastify';
import { join } from 'path';
import fastifyView from '@fastify/view';
import ejs from 'ejs';
import { AppModule } from './app.module';

interface ViteManifestEntry {
  file: string;
  src?: string;
  isEntry?: boolean;
  css?: string[];
  imports?: string[];
}

type ViteManifest = Record<string, ViteManifestEntry>;

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter()
  );

  // Register view engine
  await app.register(fastifyView, {
    engine: {
      ejs: ejs,
    },
    root: join(__dirname, '..', 'views')
  });

  // Load manifest.json ke locals supaya bisa dipakai di semua ejs
  const manifestPath = join(
    __dirname,
    '..',
    'public',
    '.vite',
    'manifest.json',
  );

  const manifest = JSON.parse(
    readFileSync(manifestPath, 'utf-8'),
  ) as ViteManifest;
  const entry = manifest['main.js'];

  app.use((req: Request, res: Response, next: NextFunction) => {
    res.locals.viteEntry = entry;
    next();
  });

  await app.listen(3000);
}

Usage

In your controllers

import { Controller, Get } from '@nestjs/common';
import { Render } from 'inertianest';

@Controller('users')
export class UsersController {
  @Post('create')
  @Render('Users/Create')
  async create() {
    const user = await this.userService.create();
    
    return {
      statusCode: 201,
      flash: { message: 'User created successfully' },
      viewData: { title: 'Create User' },
      props: { user }
    };
  }
}

Base View Template

For Express (views/app.ejs):

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <!-- Your assets here -->

    <% if (process.env.NODE_ENV==='development' ) { %>
        <!-- Scripts for development -->
        <script type="module" src="http://localhost:5173/@vite/client"></script>
        <script type="module" src="http://localhost:5173/main.js"></script>
    <% } else { %>
        <!-- Assets for production taken from generated manifest file -->
        <% if (viteEntry.css) { %>
            <% viteEntry.css.forEach(function (cssFile) { %>
                <link rel="stylesheet" href="/<%= cssFile %>">
            <% }); %>
        <% } %>
        <script type="module" src="/<%= viteEntry.file %>"></script>
    <% } %>
</head>

<body>
    <div id="app" data-page='<%- inertiaData %>'></div>
    <!-- Your app scripts here -->
</body>

</html>

For Fastify (views/app.ejs):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    <!-- Your assets here -->

    <% if (process.env.NODE_ENV==='development' ) { %>
        <!-- Scripts for development -->
        <script type="module" src="http://localhost:5173/@vite/client"></script>
        <script type="module" src="http://localhost:5173/main.js"></script>
    <% } else { %>
        <!-- Assets for production taken from generated manifest file -->
        <% if (viteEntry.css) { %>
            <link rel="stylesheet" href="/<%= viteEntry.css[0] %>">
        <% } %>
        <script type="module" src="/<%= viteEntry.file %>"></script>
    <% } %>
</head>
<body>
    <div id="app" data-page='<%= inertiaData %>'></div>
    <!-- Your app scripts here -->
</body>
</html>

Flash Messages

You can use the @Flash() decorator to set flash messages:

import { Controller, Post } from '@nestjs/common';
import { Flash } from 'inertianest';

@Controller()
export class AppController {
  @Post('submit')
  @Flash({ message: 'Data saved successfully!' })
  @Render('Result')
  submit() {
    return { success: true };
  }
}

Build

"build": "nest build && cd client && vite build",
"dev": "NODE_ENV=development concurrently \"npm:start:dev\" \"npm:start:client\" -c \"blue,green\" -k",
"start:client": "cd client && vite",

Vue Depedencies

    "@inertiajs/inertia",
    "@inertiajs/progress",
    "@inertiajs/vue3",
    "concurrently",
    "ejs",
    // "laravel-vite-plugin",
    "vue",
    "unplugin-auto-import",
    "unplugin-icons",
    "unplugin-vue-components",
    "vite",
    "vite-svg-loader",
    "@vitejs/plugin-vue",

Vue Setup

create client/main.js

import { createApp, h } from 'vue';
import { InertiaProgress } from '@inertiajs/progress';
import { createInertiaApp } from '@inertiajs/vue3';
// import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';

// Initialize progress bar
InertiaProgress.init();

// Create Inertia app
createInertiaApp({
  title: (title) => `${title} - Example`,
  
  resolve: name => {
    const pages = import.meta.glob('./pages/**/*.vue', { eager: true })
    return pages[`./pages/${name}.vue`]
  },
  // resolve: (name) => resolvePageComponent(`./pages/${name}.vue`, import.meta.glob('./pages/**/*.vue')),

  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el)
  },
});

create client/vite.config.js

import { resolve } from 'path';
import Vue from '@vitejs/plugin-vue';
import AutoImport from 'unplugin-auto-import/vite';
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';
import Components from 'unplugin-vue-components/vite';
import { HeadlessUiResolver } from 'unplugin-vue-components/resolvers';
import SvgLoader from 'vite-svg-loader';

export default () => ({
  publicDir: 'fake_dir_so_nothing_gets_copied',
  build: {
    manifest: true,
    outDir: resolve(__dirname, '../public'),
    rollupOptions: {
      input: './main.js',
    },
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, '.'),
    },
  },
  plugins: [
    Vue({}),

    AutoImport({
      imports: [
        'vue',
        {
          '@inertiajs/inertia': ['Inertia'],
          '@inertiajs/vue3': ['usePage', 'useForm'],
        },
      ],
      dts: false,
    }),

    // https://github.com/antfu/unplugin-vue-components
    Components({
      dirs: ['components', 'layouts'],
      dts: false,
      resolvers: [
        IconsResolver({
          componentPrefix: '',
        }),
        HeadlessUiResolver(),
        (name) => {
          if (['Head', 'Link'].includes(name)) {
            return {
              from: '@inertiajs/vue3',
              name: name,
            };
          }
        },
      ],
    }),

    Icons(),

    SvgLoader(),
  ],
});

create client/layouts/Main.vue

<script>

</script>

<template>
    <main>
        <slot />
    </main>
</template>

create client/pages/Home.vue

<script setup>
import Main from '@/layouts/Main.vue';
defineProps({
  title: String,
  description: String,
});
</script>

<template>
  <Main>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </Main>
</template>

Shared Props

create inertia.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import { InertiaExpress } from 'inertianest';

@Injectable()
export class InertiaMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    if (!res.inertia) {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      res.inertia = new InertiaExpress(req as any, res as any);
    }

    res.inertia.share({
      content: 'contoh content',
    });

    next();
  }
}

modify home.module.ts

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { HomeController } from './home.controller';
import { InertiaModule } from 'inertianest';
import { InertiaMiddleware } from 'src/inertia/inertia.middleware';

@Module({
  imports: [
    InertiaModule.register({
      adapter: 'express',
      view: 'app', // Your base view file name
      version: '1.0',
    }),
  ],
  controllers: [HomeController],
})
export class HomeModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(InertiaMiddleware).forRoutes('home');
  }
}

modify client/pages/Home.vue

<script setup>
import Main from '@/layouts/Main.vue';
import { usePage } from '@inertiajs/vue3';
defineProps({
  title: String,
  description: String,
});

const page = usePage();
const content = computed(() => page.props.content);
</script>

<template>
  <Main>
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
    <p>{{ content }}</p>
  </Main>
</template>

Add TailwindCSS

install

npm install tailwindcss @tailwindcss/vite

modify vite.config.js

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default () => ({
  ...
  
  plugins: [
    tailwindcss(),
  ],

})

create client/style.css

@import "tailwindcss";

add to client/main.js

import './style.css';

Without Decorator

The better approach is to use the @Render() decorator and let NestJS handle the response lifecycle, which ensures all these features work as expected:

example express

import express from 'express';
import { inertia } from 'inertianest/express';

const app = express();

// Setup inertia middleware
app.use(inertia({
  view: 'app',
  version: '1.0',
  manifest: {}
}));

// Setelah middleware terpasang, res.inertia tersedia di semua route
app.get('/', (req, res) => {
  res.inertia.render('Home', { message: 'Hello' });
});

example fastify

import fastify from 'fastify'
import { inertia } from 'inertianest/fastify'

const app = fastify()

// Register inertia middleware
app.addHook('onRequest', inertia({
  view: 'app',
  version: '1.0',
  manifest: {}
}))

// Sekarang reply.inertia tersedia di semua route
app.get('/', async (request, reply) => {
  return reply.inertia.render('Home', { message: 'Hello' })
})

Lazy and Defer

Lazy Props:

  • Only load when explicitly requested via partial reloads
  • Will be excluded from initial page load
  • Good for data that isn't immediately needed
  • Must be manually triggered to load via Inertia.reload()

Deferred Props:

  • Automatically load after initial page render
  • Initially returns null on first render
  • Then automatically fetches data without manual intervention
  • Good for non-critical data that can load after page is visible
@Controller('dashboard')
export class DashboardController {
  @Get()
  @Render('Dashboard/Index')
  async index() {
    return {
      props: {
        // Critical data - loads immediately
        user: await this.getUser(),
        
        // Deferred - loads automatically after page render
        recentActivity: Inertia.deferred(async () => {
          return await this.getRecentActivity();
        }),
        
        // Lazy - only loads when manually requested
        analytics: Inertia.lazy(async () => {
          return await this.getAnalytics();
        })
      }
    };
  }
}

And then in your Vue component, you can request partial reloads like this:

<script setup>
import { onMounted } from 'vue'
import { Inertia } from '@inertiajs/inertia'

// Deferred props load automatically - no manual trigger needed
// Will initially be null, then populate automatically

// Lazy props need manual triggering
const loadAnalytics = () => {
  Inertia.reload({ only: ['analytics'] })
}
</script>

<template>
  <div>
    <!-- Always available immediately -->
    <h1>Welcome {{ user.name }}</h1>
    
    <!-- Initially null, loads automatically -->
    <div v-if="recentActivity">
      <h2>Recent Activity</h2>
      <ActivityList :items="recentActivity" />
    </div>
    <div v-else>
      Loading activity...
    </div>
    
    <!-- Only loads when button clicked -->
    <button @click="loadAnalytics" v-if="!analytics">
      Load Analytics
    </button>
    <AnalyticsChart v-else :data="analytics" />
  </div>
</template>

Merge

Now here's an example of how to use both deferred props and the new static merge functionality in a controller:

@Controller('posts')
export class PostsController {
  @Get()
  @Render('Posts/Index')
  async index() {
    return {
      props: {
        // Regular props load immediately
        posts: await this.getPosts(),
        
        // Deferred props load only when requested
        comments: Inertia.deferred(async () => {
          return await this.getComments();
        }),
        
        // Use static merge to combine data
        users: Inertia.merge('users', [
          { id: 1, name: 'New User' }
        ])
      }
    };
  }

  private async getPosts() {
    return [
      { id: 1, title: 'First Post' },
      { id: 2, title: 'Second Post' }
    ];
  }

  private async getComments() {
    // Simulate slow API call
    await new Promise(resolve => setTimeout(resolve, 2000));
    return [
      { id: 1, text: 'Great post!' },
      { id: 2, text: 'Thanks for sharing' }
    ];
  }
}

And here's how to use it in your Vue component:

<script setup lang="ts">
import { Inertia } from '@inertiajs/inertia'

// Load deferred comments when needed
const loadComments = () => {
  Inertia.reload({ only: ['comments'] })
}
</script>

<template>
  <div>
    <!-- Posts load immediately -->
    <div v-for="post in posts" :key="post.id">
      {{ post.title }}
    </div>

    <!-- Comments are null until loaded -->
    <button @click="loadComments" v-if="!comments">
      Load Comments
    </button>
    
    <div v-else>
      <div v-for="comment in comments" :key="comment.id">
        {{ comment.text }}
      </div>
    </div>

    <!-- Users array will be merged with existing data -->
    <div v-for="user in users" :key="user.id">
      {{ user.name }}
    </div>
  </div>
</template>

Development

npm run build

add this in package.json

"inertianest": "file:../inertianest",

Security

If you've found a bug regarding security, please mail [email protected] instead of using the issue tracker.

License

The MIT License (MIT). Please see License File for more information.