Hot Module Replacement (HMR)
KickJS uses Vite's HMR to provide zero-downtime reloading during development. When you save a file, the Express handler is rebuilt and swapped on the existing HTTP server. Database pools, Redis connections, and port bindings survive across reloads.
How It Works
The kick dev command runs npx vite-node --watch src/index.ts. Vite watches your source files and triggers module re-execution when changes are detected.
The bootstrap() Function
The bootstrap() function from @forinda/kickjs-http handles the entire HMR lifecycle:
import { bootstrap } from '@forinda/kickjs-http'
import { modules } from './modules'
bootstrap({ modules })Internally, bootstrap() does the following:
First boot:
- Registers global error handlers (
uncaughtException,unhandledRejection) - Registers shutdown handlers for
SIGINTandSIGTERM - Creates a new
Applicationinstance - Stores it on
globalThis.__app - Calls
app.start()which runssetup()then starts the HTTP server - Calls
import.meta.hot.accept()to tell Vite this module handles its own updates
Subsequent reloads (HMR):
- Detects
globalThis.__appalready exists - Calls
app.rebuild()instead ofapp.start() - Returns immediately -- no new server is created
The rebuild() Method
Application.rebuild() performs a surgical swap:
rebuild(): void {
Container.reset()
this.container = Container.getInstance()
this.app = express()
this.setup()
if (this.httpServer) {
this.httpServer.removeAllListeners('request')
this.httpServer.on('request', this.app)
}
}Step by step:
- Reset the DI container -- clears all singletons so they are re-created with fresh code
- Get a fresh container instance
- Create a new Express app -- clean middleware and route stack
- Run the full setup pipeline -- adapters, middleware, modules, routes, error handlers
- Swap the request handler -- remove old listeners on the
http.Server, attach the new Express app
What Is Preserved
| Preserved across HMR | Rebuilt on each reload |
|---|---|
http.Server instance | Express app |
| Port binding | Middleware stack |
| TCP connections | Route table |
| Database connection pools | DI container singletons |
| Redis clients | Controller instances |
| Socket.IO server | Service instances |
The http.Server is created once during the first app.start() call and never recreated. Only the request handler function is swapped, so existing connections and listeners remain intact.
globalThis Storage
KickJS uses globalThis to persist state across Vite module re-executions:
globalThis.__app-- the Application instance (created once, rebuilt on HMR)globalThis.__kickBootstrapped-- flag to prevent re-registering process handlers
This pattern works because globalThis survives Vite's module invalidation, while module-level variables are reset.
Vite HMR Acceptance
The key line is import.meta.hot.accept() at the end of bootstrap():
const meta = import.meta as any
if (meta.hot) {
meta.hot.accept()
}This tells Vite that the entry module handles its own updates. Without this call, Vite would perform a full server restart on every change.
Configuring Vite
A minimal vite.config.ts for HMR support:
import { defineConfig } from 'vite'
export default defineConfig({
build: {
target: 'node20',
ssr: true,
rollupOptions: {
input: 'src/index.ts',
},
},
})The kick dev command uses vite-node --watch which reads this config automatically. No additional HMR configuration is needed.
Graceful Shutdown
When the process receives SIGINT or SIGTERM, bootstrap() calls app.shutdown() which:
- Runs all adapter
shutdown()methods concurrently viaPromise.allSettled - Closes the HTTP server
- Exits the process
Adapter shutdown failures are logged but do not prevent other adapters from cleaning up.