Skip to main content

Developing Without Server Restart

Developing with Sonamu is like this:
  1. Modify code
  2. Save (Cmd+S)
  3. Done! πŸ‘ˆ Instantly reflected
No need to refresh the browser, restart the server, or run build commands.

Actual Development Speed Difference

Without HMR (Traditional approach):
# Entity modification β†’ Server restart β†’ Test
1. Modify Entity in Sonamu UI
2. Ctrl+C (stop server)
3. pnpm dev (restart server)
4. Wait 20-30 seconds...
5. Test

30 seconds wasted each iteration 😫
With HMR (Sonamu approach):
# Entity modification β†’ Instant reflection
1. Modify Entity in Sonamu UI
2. πŸ”„ Invalidated log appears in console after 2 seconds
3. Test

2 seconds is enough even with iterations πŸš€
If you modify Entity/API 50 times a day?
  • Without HMR: 50 Γ— 30 seconds = 25 minutes wasted
  • With HMR: 50 Γ— 2 seconds = 1.7 minutes

Difference from Other Frameworks

FrameworkHMR SupportAuto-generated Code SyncEntity Change Reflection
NestJS❌ None-Manual restart required
Express❌ None-nodemon restart
Fastify⚠️ Partial support-Manual restart required
Sonamuβœ… Full supportβœ… Automaticβœ… Instant reflection

What Makes Sonamu Special

Typical Node.js HMR:
  • Simply reloads files
  • Auto-generated code needs manual management
  • Multiple files need manual modification on Entity change
Sonamu HMR:
  • File reload + automatic Syncer execution
  • Entity change β†’ All related code auto-generated/updated
  • All dependent files auto-reloaded
  • APIs auto-re-registered

Real Development Scenarios

Scenario 1: Adding Entity Field

Adding a nickname field to User Entity: Step 1: Add field in Sonamu UI
// Add nickname to User Entity
nickname: StringProp({ maxLength: 50 })
Step 2: Instantly on save
  • user.entity.ts auto-updated
  • user.types.ts types auto-generated
  • user.zod.ts Zod schema auto-generated
  • UserModel auto-reloaded
  • All UserApi methods auto-re-registered
  • Frontend UserService auto-updated
Step 3: Check console
πŸ”„ Invalidated:
- src/application/user/user.entity.ts
- src/application/user/user.model.ts (with 8 APIs)
- src/application/user/user.api.ts

βœ… All files are synced!
Step 4: Test
# API call without server restart
curl http://localhost:3000/api/user/1
{
  "id": 1,
  "name": "John",
  "nickname": null  // πŸ‘ˆ New field instantly reflected!
}
⏱️ With HMR: 2 seconds ⏱️ Without HMR: 30 seconds (server restart + recompile)

Scenario 2: Modifying API Logic

Adding pagination and search to UserApi.list():
// user.api.ts before modification
@api({ httpMethod: "GET" })
async list() {
  return UserModel.findMany();
}
// user.api.ts after modification
@api({ httpMethod: "GET" })
async list(listParams: ListParams & { keyword?: string }) {
  const where = listParams.keyword
    ? { name: { $like: `%${listParams.keyword}%` } }
    : undefined;

  return UserModel.findMany({
    where,
    limit: listParams.num,
    offset: (listParams.page - 1) * listParams.num,
  });
}
On save:
πŸ”„ Invalidated:
- src/application/user/user.api.ts

✨ API re-registered: GET /api/user/list
Testable immediately in Postman/Thunder Client!
GET /api/user/list?keyword=john&page=1&num=10
# Works immediately βœ…

Scenario 3: Adding Model Business Logic

Adding user active status check logic:
// user.model.ts
export class UserModel extends BaseModel {
  static async findActive() {
    return this.findMany({
      where: { status: "active", deletedAt: null }
    });
  }

  isActive(): boolean {
    return this.status === "active" && !this.deletedAt;
  }
}
Save β†’ 2 seconds later β†’ Available in other APIs immediately:
// post.api.ts
@api({ httpMethod: "POST" })
async create(body: PostForm) {
  const user = await UserModel.findById(body.userId);

  if (!user.isActive()) {  // πŸ‘ˆ Use the method just added!
    throw new BadRequestError("User is not active");
  }

  return PostModel.save(body);
}

Sonamu HMR’s Innovation: Syncer Integration

Typical Node.js HMR is simply β€œFile changed? Let’s reload it.” But Sonamu is different.

Problem: The Auto-generated Code Dilemma

Sonamu auto-generates numerous code from Entities:
  • TypeScript type files (*.types.ts)
  • Zod schemas (*.zod.ts)
  • API route registration
  • Frontend Service classes
If Syncer and HMR operated separately:
User Entity modified
  ↓
Syncer starts generating files...
  ↓
HMR detects and starts reloading... ← 🚨 Still generating!
  ↓
Timing mismatch β†’ Errors occur

Sonamu’s Solution

Syncer directly controls HMR:
// syncer.ts
async syncFromWatcher(event: string, diffFilePath: AbsolutePath) {
  // 1. Request file invalidation from HMR
  const invalidatedPaths = await hot.invalidateFile(diffFilePath, event);

  // 2. Auto-generated code sync complete (order guaranteed!)
  await this.doSyncActions([diffFilePath]);

  // 3. Now safely reload modules
  await this.autoloadTypes();
  await this.autoloadModels();
  await this.autoloadApis();
  await this.autoloadWorkflows();

  // 4. Done!
  this.eventEmitter.emit("onHMRCompleted");
}
Because order is guaranteed:
  1. Code generation completes fully
  2. Then reload begins
  3. Latest code is always loaded βœ…
This is why Sonamu HMR is not just β€œfast” but safe and reliable.

HMR Architecture

Key Components

Sonamu’s HMR system consists of three core components: 1. @sonamu-kit/hmr-hook A package forked from hot-hook and customized for Sonamu.
// hmr-hook-register.ts
if (process.env.HOT === "yes" && process.env.API_ROOT_PATH) {
  const { hot } = await import("@sonamu-kit/hmr-hook");

  await hot.init({
    rootDirectory: process.env.API_ROOT_PATH,
    boundaries: [`./src/**/*.ts`],  // All .ts files are HMR targets
  });

  console.log("πŸ”₯ HMR-hook initialized");
}
Uses Node.js’s Module Loader API to intercept the module loading process. 2. Dependency Tree Tracks dependencies between files in a tree structure:
user.model.ts
  β”œβ”€ user.api.ts
  β”œβ”€ post.api.ts
  └─ admin.api.ts
When user.model.ts changes, 3 dependent files are also reloaded. Example: When user.model.ts changes, 3 dependent files (user.api.ts, post.api.ts, admin.api.ts) are also reloaded. 3. Syncer Detects file changes and coordinates HMR and code generation:
// File watcher
watcher.on("change", async (filePath) => {
  await syncer.syncFromWatcher("change", filePath);
});

HMR Process Details

When a developer modifies a file, the following process runs automatically:

1. File Change Detection

// Monitor filesystem with chokidar
watcher.on("change", (filePath) => {
  console.log(`File changed: ${filePath}`);
});

2. Module Invalidation

// Remove ESM cache
const invalidatedPaths = await hot.invalidateFile(diffFilePath, "change");
// ["src/user/user.model.ts", "src/user/user.api.ts", ...]
Removes ESM import cache for the changed file and traverses the Dependency Tree to remove caches for all dependent files.

3. Auto-generated Code Sync

When Entity or Model changes, Syncer auto-generates related code:
await this.doSyncActions([diffFilePath]);
Generated files:
  • TypeScript type file (user.types.ts)
  • Zod schema (user.zod.ts)
  • Frontend Service (UserService.ts)

4. Module Reload

await this.autoloadTypes();
await this.autoloadModels();
await this.autoloadApis();
await this.autoloadWorkflows();
Invalidated modules have their caches removed, so the latest code is loaded.

5. API Re-registration

When a Model file changes, all APIs for that Model are re-registered:
removeInvalidatedRegisteredApis(invalidatedPath: AbsolutePath) {
  const entityId = EntityManager.getEntityIdFromPath(invalidatedPath);
  const toRemove = registeredApis.filter((api) => api.modelName === `${entityId}Model`);

  for (const api of toRemove) {
    registeredApis.splice(registeredApis.indexOf(api), 1);
  }

  return toRemove;
}
Console output:
πŸ”„ Invalidated:
- src/user/user.model.ts (with 8 APIs)

Differences from Original hot-hook

Sonamu’s @sonamu-kit/hmr-hook forked the original hot-hook with the following improvements:

1. Allow Variable-based Dynamic Import

// Original hot-hook: ❌ Not possible
const modelPath = `./models/${entityId}.model`;
await import(modelPath);  // Error!

// Sonamu hmr-hook: βœ… Possible
const modelPath = `./models/${entityId}.model`;
await import(modelPath);  // Works
Essential for Sonamu’s Entity-based structure where paths must be dynamically generated.

2. Allow Static Import Between Boundaries

// user.model.ts (boundary)
import { PostModel } from "./post.model";  // Original: ❌ / Sonamu: βœ…

export class UserModel extends BaseModel {
  async getPosts() {
    return PostModel.findMany({ where: { userId: this.id } });
  }
}
Static imports allowed since references between Models are frequent.

3. Filesystem Watcher Disabled

The original uses its own file watcher, but Sonamu uses only Syncer’s watcher:
// When Syncer detects file change, notify HMR directly
await hot.invalidateFile(filePath, "change");
This precisely controls the order of code generation and module invalidation.

4. Improved Integration with ts-loader

Resolved path mismatches between compiled dist/*.js paths and original src/*.ts paths.

Performance Optimization

Selective Invalidation

Only the changed file and its dependencies are invalidated, preventing unnecessary reloads:
// user.types.ts change β†’ only user.model.ts reloads
// Not the entire project, just affected files!

Checksum-based Change Detection

Only files with actual content changes are processed:
const changedFiles = await findChangedFilesUsingChecksums();
if (changedFiles.length === 0) {
  console.log(chalk.black.bgGreen("All files are synced!"));
  return;
}
Even if a file is saved, no sync occurs if content is unchanged.

Graceful Shutdown Handling

Prevents process termination during sync operations:
await runWithGracefulShutdown(
  async () => {
    await this.doSyncActions(changedFiles);
    await renewChecksums();
  },
  { whenThisHappens: "SIGUSR2", waitForUpTo: 20000 },
);
Waits up to 20 seconds for sync completion even when receiving Nodemon restart signal.

Enabling HMR

Automatically enabled in development mode:
pnpm dev  # HOT=yes is auto-set
Can also be controlled via environment variables:
# Enable HMR
HOT=yes pnpm dev

# Disable HMR (for debugging)
HOT=no pnpm dev
HMR is automatically disabled in production builds, and static builds are generated.

Development Tips

Check HMR Status

// Check logs printed in terminal
πŸ”₯ HMR-hook initialized

// On file change
πŸ”„ Invalidated:
- src/user/user.model.ts (with 8 APIs)
- src/user/user.api.ts

βœ… All files are synced!

If It Seems Slow

If changes seem slow to reflect after file modification:
# Check dependency tree
const dump = await hot.dump();
console.log(`Total modules: ${dump.length}`);
console.log(`Boundaries: ${dump.filter(d => d.boundary).length}`);
Improve import structure if there are too many dependencies.

Changes Requiring Restart

Server restart is required when modifying these files:
  • sonamu.config.ts - Configuration file
  • .env - Environment variables
  • package.json - Dependencies
These files are marked with import.meta.hot?.decline() and automatically require restart on change.