JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 737
  • Score
    100M100P100Q101702F
  • License MIT

NestJS-compatible framework for edge runtimes, powered by Hono

Package Exports

  • @velajs/vela
  • @velajs/vela/internal
  • @velajs/vela/schedule-node
  • @velajs/vela/streaming

Readme

@velajs/vela

npm version CI License: MIT

NestJS-compatible framework for edge runtimes, powered by Hono.

Install

pnpm add @velajs/vela

Quick Start

import { VelaFactory, Controller, Get, Module, Injectable } from '@velajs/vela';

@Injectable()
class AppService {
  getHello() {
    return { message: 'Hello from the edge!' };
  }
}

@Controller('/app')
class AppController {
  constructor(private appService: AppService) {}

  @Get('/')
  hello() {
    return this.appService.getHello();
  }
}

@Module({
  controllers: [AppController],
  providers: [AppService],
})
class AppModule {}

const app = await VelaFactory.create(AppModule);
export default app; // Works on Cloudflare Workers, Deno, Bun, etc.

Features

  • Decorator-based controllers@Controller, @Get, @Post, @Put, @Patch, @Delete
  • Dependency injection@Injectable, @Inject, InjectionToken, singleton/transient/request scopes
  • Modules@Module with imports, exports, controllers, providers
  • Guards@UseGuards with CanActivate interface
  • Pipes@UsePipes, built-in ParseIntPipe, ParseBoolPipe, ZodValidationPipe, etc.
  • Interceptors@UseInterceptors with NestInterceptor interface
  • Exception filters@UseFilters, @Catch, built-in HTTP exceptions
  • Middleware@UseMiddleware for Hono-native middleware
  • Custom metadata@SetMetadata + Reflector
  • Custom param decoratorscreateParamDecorator
  • Route versioning@Controller({ version: '1' }) + @Version('2')
  • Global prefixapp.setGlobalPrefix('/api')
  • Lifecycle hooksOnModuleInit, OnApplicationBootstrap, OnModuleDestroy
  • CRUD integration — Optional @velajs/crud package

Edge Runtime Compatibility

Vela runs on any runtime that supports the Web Standards API:

  • Cloudflare Workers
  • Deno Deploy
  • Bun
  • Node.js 20+
  • Vercel Edge Functions

No Node.js-specific APIs (node:fs, Buffer, process) are used.

Edge-safe contract

The main export (@velajs/vela) is edge-safe by contract — no node:* imports, no Buffer, no process, no setInterval, no Bun.serve. This is enforced in CI by src/__tests__/edge-runtime-audit.test.ts, which fails the build if any file under src/ references a forbidden API.

One subpath, @velajs/vela/schedule-node, is an opt-in Node/Bun adapter for setInterval-based job execution. It uses runtime-specific APIs by design and is excluded from the edge-runtime audit. Edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge) should not import it — use platform cron triggers instead (e.g., @velajs/cloudflare ≥ 0.2.0 dispatches @Cron jobs via the Workers scheduled() handler).

// Node / Bun only — opt-in
import { ScheduleNodeModule } from '@velajs/vela/schedule-node';

The other subpaths (@velajs/vela/internal, @velajs/vela/streaming) follow the main export's edge-safe contract.

Dynamic modules

Configurable modules use forRoot (sync) and forRootAsync (DI-resolved):

@Module({
  imports: [
    CacheModule.forRoot({ ttl: 60 }),
    HttpModule.forRoot({ baseURL: 'https://api.example.com' }),
    ConfigModule.forRootAsync({
      useFactory: async (loader: ConfigLoader) => loader.load(),
      inject: [ConfigLoader],
    }),
  ],
})
class AppModule {}

Identity model

Each DynamicModule has an optional key?: string that discriminates one instance from another. First-party modules derive key: stableHash(options) automatically inside forRoot — so the same options always dedup, and distinct options register as distinct instances:

// Same options → dedup (one CacheModule instance, ttl: 60)
imports: [
  CacheModule.forRoot({ ttl: 60 }),
  CacheModule.forRoot({ ttl: 60 }),
]

// Different options → two distinct instances coexist
imports: [
  CacheModule.forRoot({ ttl: 60 }),
  CacheModule.forRoot({ ttl: 120 }),
]

When a consumer module imports two instances both exporting the same logical token, the resolver throws MultipleProvidersFoundError with both candidate ids — resolve the ambiguity by importing only one, or use a per-instance accessor exposed by the module. Most apps with a single instance never hit this.

Custom modules can use the same pattern via the public helpers:

import { defineDynamicModule, stableHash } from '@velajs/vela';

class MyModule {
  static forRoot(options: MyOptions): DynamicModule {
    return defineDynamicModule({
      module: MyModule,
      key: stableHash(options),    // or pass an explicit key
      providers: [/* ... */],
      exports: [/* ... */],
    });
  }
}

forRootAsync callers should pass key explicitly when the same module needs multiple async instances — factories aren't structurally hashable.

Companion packages

Package Purpose
@velajs/cloudflare Cloudflare Workers adapter — typed services for KV, D1, R2, Queues, DO, AI, Vectorize, Hyperdrive
@velajs/crud NestJS-style CRUD controllers on top of hono-crud
@velajs/testing Test.createTestingModule() with overrideProvider/Guard/Pipe/Interceptor/Filter
pnpm add @velajs/testing -D
pnpm add @velajs/cloudflare @cloudflare/workers-types
pnpm add @velajs/crud hono-crud @hono/zod-openapi zod

/internal subpath (for plugin authors)

Framework primitives — MetadataRegistry, Container, RouteManager, ModuleLoader, ComponentManager, VelaApplication, bindAppProviders, APP_* tokens — are exposed at @velajs/vela/internal. This is the stable target for plugin packages that need to reach below the public API.

import { MetadataRegistry, Container } from '@velajs/vela/internal';

The public root barrel still exports MetadataRegistry (used by tests for clear() between cases). Everything else lives at /internal only.

License

MIT