DEV Community

Cover image for HazelJS Core Architecture: What We Built Differently
Muhammad Arslan
Muhammad Arslan

Posted on

HazelJS Core Architecture: What We Built Differently

How the HazelJS core framework approaches the dev server, bootstrap, and production readiness—and what sets it apart from NestJS, Express, and Fastify.


Overview

HazelJS is a TypeScript-first Node.js framework built for AI-native and enterprise applications. The core (@hazeljs/core) provides dependency injection, decorator-based routing, middleware, and production features—without the weight of a custom runtime or complex build pipeline.

This post focuses on the core architecture: how the dev server works, what we did differently, and the competitive advantages that most Node.js frameworks lack out of the box.


1. The Dev Server: Simple and Fast

No Custom Dev Runtime

HazelJS does not ship a custom dev server or bundler. Instead, it uses standard Node.js tooling:

# From hazel new or package.json
"dev": "ts-node-dev --respawn --transpile-only src/index.ts"
Enter fullscreen mode Exit fullscreen mode

Why this approach?

  • Zero lock-in — Your app runs on plain Node.js. No framework-specific dev runtime to debug or upgrade.
  • Fast startupts-node-dev with --transpile-only skips type-checking on reload; restarts are sub-second.
  • Familiar — Developers already know ts-node, ts-node-dev, or tsx. No new concepts.
  • Flexible — Swap to tsx, nodemon, or node --watch without changing framework code.

What Competitors Do

Framework Dev Server Approach
NestJS nest start --watch (CLI wraps ts-node-dev or webpack)
Express No built-in dev; you add nodemon or ts-node-dev yourself
Fastify Same as Express—manual setup
HazelJS CLI scaffolds ts-node-dev --respawn --transpile-only; hazel start -d runs it

HazelJS keeps the dev experience explicit and minimal: you see exactly what runs, and you can change it.


2. Bootstrap and Application Lifecycle

Single Entry Point

import { HazelApp } from '@hazeljs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = new HazelApp(AppModule);
  await app.listen(3000);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

No NestFactory, no createApp builder chain. You instantiate HazelApp with your root module and call listen().

What Happens on listen()

  1. Module resolution — The framework recursively collects controllers from the root module and its imports.
  2. Route registration — All @Controller classes are registered with the router; decorators define routes.
  3. HTTP server — A native Node.js http.Server is created (no Express dependency in core).
  4. Health endpoints/health, /ready, and /startup are registered automatically.
  5. Graceful shutdown — Signal handlers (SIGTERM, SIGINT, SIGUSR2) are attached.
  6. Startup output — Local and network URLs are printed, plus health endpoint hints.

Built-in Health Probes (Kubernetes-Ready)

Most frameworks require you to add health checks manually. HazelJS ships them:

Endpoint Purpose Use Case
/health Liveness — is the process alive? Kubernetes liveness probe
/ready Readiness — can it accept traffic? Kubernetes readiness probe
/startup Startup — has initialization finished? Kubernetes startup probe

You get memory and event-loop checks by default. Custom checks are one line:

app.registerHealthCheck({
  name: 'database',
  check: async () => ({ status: 'healthy', message: 'DB connected' }),
  critical: true,
  timeout: 5000,
});
Enter fullscreen mode Exit fullscreen mode

Competitive gap: Express and Fastify have no built-in health system. NestJS has @nestjs/terminus, but it's a separate package and requires setup. HazelJS includes it in core.


3. Graceful Shutdown

ShutdownManager

When the process receives SIGTERM or SIGINT, HazelJS runs registered shutdown handlers in order:

app.registerShutdownHandler({
  name: 'http-server',
  handler: async () => {
    await app.close();
  },
  timeout: 10000,
});
Enter fullscreen mode Exit fullscreen mode

The framework:

  • Handles SIGTERM, SIGINT, SIGUSR2
  • Handles uncaughtException and unhandledRejection (logs, runs handlers, then exits)
  • Enforces per-handler timeouts to avoid hanging
  • Has a global shutdown timeout (default 30s) before forcing exit

The HTTP server is registered as a shutdown handler automatically when listen() is called, so connections drain before the process exits.

Competitive gap: Express has no built-in graceful shutdown. Fastify has close(), but you wire signals yourself. NestJS has enableShutdownHooks(), but it's opt-in and less configurable. HazelJS makes it first-class.


4. Native Node.js HTTP Server: What No Other Framework Does

The Key Difference

HazelJS uses Node.js http.Server directly—no Express, no Fastify, no third-party HTTP abstraction. The request handler is a single callback that receives IncomingMessage and ServerResponse from Node's built-in http module:

import { Server, IncomingMessage, ServerResponse } from 'http';

this.server = new Server(async (req: IncomingMessage, res: ServerResponse) => {
  // Direct handling: route match → controller → response
});
Enter fullscreen mode Exit fullscreen mode

How Other Frameworks Work

Framework HTTP Layer What It Adds
Express Wraps Node http Middleware stack (every request passes through body-parser, cookie-parser, etc.), routing layer, req/res extensions
NestJS Uses Express (or Fastify) by default Nest's own middleware + Express/Fastify stack + DI, guards, interceptors
Fastify Custom HTTP abstraction Schema validation, plugin system, request/reply wrappers—faster than Express but still an abstraction
HazelJS Raw Node.js http.Server Router + DI; no middleware stack, no extra HTTP wrapper

Express and NestJS sit on top of Node's HTTP with multiple abstraction layers. Fastify is faster than Express but still introduces its own request/response model. HazelJS stays at the Node.js HTTP level and adds only what's needed for routing and DI.

Performance Edges

1. No middleware chain overhead

Express runs every request through a chain of middleware functions. Even a "hello world" app typically has body-parser, cookie-parser, and other middleware. Each adds a function call and potential I/O. HazelJS has no generic middleware chain—body parsing runs only for POST/PUT/PATCH and only when the route needs it.

2. Lazy body parsing

Body parsing happens only for methods that expect a body (POST, PUT, PATCH). GET and DELETE requests skip it entirely. Express's body-parser often runs on every request or is configured globally.

3. Minimal abstraction

HazelJS uses Node's native IncomingMessage and ServerResponse. There's no req.body or res.json() until the framework explicitly parses or writes. The HttpResponse wrapper is thin—it just adds status(), json(), and setHeader() for controller convenience. No deep prototype chains or proxy objects.

4. Smaller bundle and faster cold start

@hazeljs/core has no Express dependency. That means:

  • Fewer modules to load at startup
  • No Express internals (router, Layer, Route, etc.)
  • Lower memory footprint

Benchmarks typically show native Node HTTP and Fastify at ~95k–105k req/s vs Express at ~23k req/s for simple endpoints. HazelJS aligns with the native-HTTP approach: minimal layers between the TCP socket and your handler.

5. Serverless-friendly

Because core doesn't depend on Express, the same application can run on AWS Lambda or Google Cloud Functions via @hazeljs/serverless without pulling in Express. The serverless adapter invokes the router directly with an event object—no HTTP server at all in serverless mode.

Trade-offs

HazelJS does not provide Express's vast middleware ecosystem (e.g. express-session, passport). For those, you'd use dedicated packages (e.g. @hazeljs/auth for JWT) or integrate Express as a nested app if needed. The design choice is: optimize for performance and simplicity in core, and add abstractions only where they're required.


5. Request Timeout and CORS

Request Timeout

Long-running requests can be capped:

app.setRequestTimeout(30000); // 30 seconds default
Enter fullscreen mode Exit fullscreen mode

If a handler exceeds the timeout, the client gets 408 Request Timeout and the handler is not left hanging.

CORS

CORS is built-in and configurable:

app.enableCors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
});
Enter fullscreen mode Exit fullscreen mode

No cors package to add or configure manually.


6. Startup Experience

When the server starts, HazelJS prints:

Server listening on:

  → Local:    http://localhost:3000
  → Network:  http://192.168.1.10:3000

Health endpoints:
  → /health  - Liveness probe
  → /ready   - Readiness probe
  → /startup - Startup probe
Enter fullscreen mode Exit fullscreen mode

You get both local and network URLs (useful for mobile or LAN testing) and a reminder of the health endpoints. Many frameworks only log Listening on port 3000.


7. Dependency Injection and Scopes

The DI container supports three scopes:

Scope Behavior
Singleton One instance per process (default)
Transient New instance per resolution
Request One instance per HTTP request

Circular dependency detection is built in. Providers can use useClass, useValue, or useFactory.

Competitive note: NestJS has a richer DI system. HazelJS keeps it simple but sufficient for most apps—and avoids the complexity of dynamic modules and custom providers for common cases.


8. Module System

Modules are the unit of organization:

@HazelModule({
  controllers: [UserController],
  providers: [UserService],
  imports: [AuthModule, CacheModule],
  exports: [UserService],
})
export class UserModule {}
Enter fullscreen mode Exit fullscreen mode

The framework recursively collects controllers from imports, so you can compose features without manual registration. This is similar to NestJS but with a lighter metadata model.


9. What Competitors Don’t Have (Summary)

Feature Express Fastify NestJS HazelJS
Built-in health probes Via Terminus ✅ In core
Graceful shutdown Manual Opt-in ✅ First-class
Request timeout Plugin Manual ✅ Built-in
CORS built-in Plugin Via config ✅ Built-in
DI container
Decorator routing
Module system
Native Node http.Server (no Express/Fastify) N/A Custom (not raw http) ❌ (Express/Fastify)
Dev server Manual Manual CLI CLI + ts-node-dev
AI/Agent packages Community ✅ First-party

10. CLI and Scaffolding

The @hazeljs/cli provides:

  • hazel new <app> — Interactive project creation with package selection
  • hazel generate controller|service|module|... — 21 generators for all packages
  • hazel start -d — Run npm run dev
  • hazel build -w — Watch mode builds
  • hazel add — Add HazelJS packages interactively

Generated projects include:

  • ts-node-dev for hot reload
  • .env and .env.example
  • Health endpoints and graceful shutdown wired by default

Summary

HazelJS core is designed for:

  1. Simplicity — No custom dev runtime; standard Node.js and ts-node-dev.
  2. Production readiness — Health probes, graceful shutdown, and request timeout out of the box.
  3. Explicitness — You see the bootstrap flow; no magic.
  4. Modularity — Install only what you need; core stays lean.

If you're building AI-native backends or want a framework that ships production features from day one, HazelJS is worth a look. Clone the CSR agent example or run hazel new my-app -i to get started.

Top comments (0)