Skip to content

FAQ

Answers to common questions and issues when working with HonestJS. For exhaustive edge-case coverage, see Troubleshooting.

Setup & First Run

See also: Getting Started | Configuration

Why do I need reflect-metadata?

HonestJS uses TypeScript's emitDecoratorMetadata to read constructor parameter types at runtime, which powers the dependency injection system. The reflect-metadata polyfill makes this metadata available. Import it once at the top of your entry file (main.ts) before any HonestJS imports:

typescript
import 'reflect-metadata' // must be first
import { Application } from 'honestjs'

Without it, the DI container cannot resolve constructor dependencies and you'll see errors like "constructor metadata is missing".

Which template should I use?

TemplateUse when...
blankLearning HonestJS or building something minimal
bareboneBuilding an API or a medium-sized app
mvcBuilding a full-stack app with server-rendered views

If you're coming from NestJS or Express and building an API, start with barebone.

Does HonestJS work with Node.js?

Yes. Bun is recommended for the best experience, but HonestJS runs on Node.js 18+ as well. The CLI (honestjs doctor) can check your environment.

What package managers are supported?

Bun (default), npm, pnpm, and yarn. Specify one with honestjs new my-app -p npm.

Dependency Injection

See also: DI Guide | Troubleshooting: DI

Why do I get "Cannot resolve dependency at index N"?

The DI container can only inject concrete classes. You'll see this error if a constructor parameter is typed as:

  • An interface (interfaces don't exist at runtime)
  • A primitive (string, number, boolean)
  • Object or any (too broad for reflection)

Fix: Use a concrete class type for every constructor parameter.

typescript
// Will fail - interface is erased at runtime
constructor(private userService: IUserService) {}

// Works - concrete class preserved at runtime
constructor(private userService: UserService) {}

Why do I get "not decorated with @Service()"?

Every class that participates in DI must be decorated with @Service() and listed in the module's services array.

typescript
@Service()
class UserService { ... }

@Module({
  controllers: [UserController],
  services: [UserService]   // don't forget this
})
class UserModule {}

How do I handle circular dependencies?

If ServiceA depends on ServiceB and ServiceB depends on ServiceA, you'll see: "Circular dependency detected: ServiceA -> ServiceB -> ServiceA"

Fix: Refactor to break the cycle. Common strategies:

  1. Extract shared logic into a third service
  2. Use a facade pattern
  3. Restructure so one service doesn't need the other

Are services singletons?

Yes. By default, all registered services are singletons - the same instance is shared across the entire application.

Routing & Versioning

See also: Routing Guide | Troubleshooting: Routing

How does API versioning work?

HonestJS supports versioning at three levels, each overriding the previous:

  1. Global - routing: { version: 1 } → all routes get /v1/...
  2. Controller - @Controller('users', { version: 2 }) → this controller uses /v2/...
  3. Handler - @Get('legacy', { version: 1 }) → this specific route uses /v1/...

Use VERSION_NEUTRAL for routes that should be accessible both with and without a version prefix (health checks, status endpoints).

What is VERSION_NEUTRAL?

A symbol that tells HonestJS to register a route both with and without a version prefix. For example, with global version: 1:

typescript
@Controller('health', { version: VERSION_NEUTRAL })
class HealthController {
	@Get('status')
	getStatus() {
		return { status: 'ok' }
	}
}

This makes /health/status and /v1/health/status both work.

Deep dive VERSION_NEUTRAL actually creates multiple Hono route registrations per handler (unversioned +

versioned). This means app.getRoutes() may return more entries than you expect. See Troubleshooting for details. :::

What happens with duplicate routes?

HonestJS detects duplicate routes at startup and throws: "Duplicate route detected: GET /api/v1/users (UsersController.getUsers)". Fix by changing the path, prefix, or version on one of the conflicting handlers.

How do route prefixes combine?

Prefixes are applied in order: global prefixversioncontroller routehandler path.

Example with routing: { prefix: 'api', version: 1 } and @Controller('users'):

  • @Get()/api/v1/users
  • @Get(':id')/api/v1/users/:id
  • @Get(':id/posts')/api/v1/users/:id/posts

Pipeline (Middleware, Guards, Pipes, Filters)

See also: Components Overview | Error Handling | Troubleshooting: Pipeline

What order do components execute in?

Global middleware → Controller middleware → Handler middleware
  → Global guards → Controller guards → Handler guards
    → Global pipes → Controller pipes → Handler pipes
      → Route handler
        → Exception filters (if error thrown)

When should I use a guard vs middleware?

  • Middleware runs on every matched request and can modify the request/response. Use for logging, CORS, request parsing, etc.
  • Guards return true/false to allow or deny a request. Use for authentication and authorization.

What happens when a filter throws?

If an exception filter itself throws an error, HonestJS logs "Error in exception filter..." and returns a fallback JSON error response. Keep filter logic minimal to avoid this.

Can I use Hono middleware directly?

Yes. Use @honestjs/middleware's HonoMiddleware wrapper:

typescript
import { HonoMiddleware } from '@honestjs/middleware'
import { poweredBy } from 'hono/middleware'

components: {
	middleware: [new HonoMiddleware(poweredBy())]
}

Or access the Hono instance directly via app.getApp() for raw Hono middleware registration.

Plugins

See also: Plugins Guide | Troubleshooting: Plugins

What order do plugins run in?

Plugins run in the order they appear in options.plugins. When one plugin depends on another's output (for example, API docs reading an artifact from RPC), list the producer first:

typescript
plugins: [
	new RPCPlugin(), // produces rpc.artifact
	new ApiDocsPlugin() // consumes rpc.artifact
]

What are plugin processors?

Wrapped plugin entries can have preProcessors and postProcessors that run around the plugin lifecycle hooks:

preProcessors → beforeModulesRegistered → [modules registered] → afterModulesRegistered → postProcessors

Processors receive (app, hono, ctx) where ctx is the application context (key-value store).

How do I name plugins for debug logs?

With debug: { plugins: true }, plugin names appear in logs. The framework picks a name in order:

  1. name on a wrapped entry { plugin, name: 'core' }
  2. plugin.meta?.name
  3. The class constructor name
  4. AnonymousPlugin#N for anonymous objects

Testing

See also: Testing Guide | Troubleshooting: Metadata

Which test runner should I use?

Projects created with the CLI use Vitest (via bun run test). The HonestJS framework itself uses Bun's built-in test runner (bun test). Either works.

How do I test a controller?

Use createControllerTestApplication:

typescript
import { createControllerTestApplication } from 'honestjs'

const testApp = await createControllerTestApplication({
	controller: UsersController,
	services: [UsersService]
})

const res = await testApp.request('/users')
expect(res.status).toBe(200)

How do I test a service in isolation?

Use createServiceTestContainer:

typescript
import { createServiceTestContainer } from 'honestjs'

const harness = createServiceTestContainer({
	preload: [UsersService],
	overrides: [{ provide: DatabaseService, useValue: mockDb }]
})

const svc = harness.get(UsersService)

Do I need to clean up metadata between tests?

If you're running multiple Application.create calls in the same process (common in integration tests), call MetadataRegistry.clear() in afterEach to avoid cross-test pollution. Each application captures an immutable metadata snapshot at startup, but the global registry is shared.

Configuration

See also: Configuration Guide | Troubleshooting: Startup

What does startupGuide do?

When enabled, it emits actionable hints when app initialization fails - for example, telling you that a controller is missing @Controller() or that reflect-metadata isn't imported. Use startupGuide: { verbose: true } for extra detail.

typescript
const { app, hono } = await Application.create(AppModule, {
	startupGuide: { verbose: true }
})

What does strict: { requireRoutes: true } do?

It makes startup fail if no HTTP routes were registered. Useful to catch configuration mistakes early. Without it, an app with no routes starts silently.

What does debug: true enable?

It turns on all diagnostic categories: routes, plugins, pipeline, di, and startup. You can also enable categories individually:

typescript
debug: { routes: true, plugins: true }

Setting debug: { routes: true } also enables startup-category messages (like "Application registered N routes"

and "Application startup completed") because route registration is part of the startup flow. :::

CLI

See also: CLI Reference | Troubleshooting: CLI

What does honestjs doctor check?

It verifies your environment: Bun/Node.js versions, git, available package managers, template cache status, and GitHub network connectivity.

How do I use the CLI offline?

Run honestjs new or honestjs list once with network access to populate the template cache, then use --offline for subsequent runs. If you've never cached templates, --offline will fail with "Templates not found. Run without --offline first".

How do I overwrite existing generated files?

Use --force:

bash
honestjs g controller user --force

Without --force, the CLI will refuse to overwrite an existing file.

Deployment

See also: Deployment Guide

How do I run in production?

After creating your project with the CLI:

bash
bun run build    # build the project
bun run start    # start the production server

The application listens on the port configured by your environment (typically PORT env variable, defaulting to 3000).

Does HonestJS support Docker?

Docker configuration is optional. Pass --docker to the CLI (or select it during interactive setup):

bash
honestjs new my-app --docker

This generates a Dockerfile and optional docker-compose.yml.

Released under the MIT License.