@byteholic/nelysia
v0.1.0
Published
Native Elysia modular framework
Maintainers
Readme
Nelysia
NestJS-style modular framework built natively on ElysiaJS + Bun.
Features
- 🧩
@Plugin— NestJS-style@Modulepowered by native Elysia.use() - 💉
@Service+@Inject— Lightweight DI container - 🌐
@Controller+ HTTP verbs —@Get,@Post,@Put,@Patch,@Delete - 🔌
@WsController+@Ws— Native WebSocket support with pub/sub - 🎯 Context decorators —
@Body,@Query,@Params,@Path,@Headers,@Cookie - 🛡️
@BeforeHandle— Per-route guards - 📋
@Schema— Elysiatvalidation per route - 📖
@Detail— OpenAPI / Swagger metadata - ⚙️
@Macro— Elysia.macro()as class-based decorators - 🔁 Lifecycle hooks —
onRequest,onError,onAfterResponse, etc. in@Plugin
Install
bun add @byteholic/nelysia elysia
bun add -d bun-typestsconfig.json requirements
{
"compilerOptions": {
"experimentalDecorators": true,
"strict": true
}
}Quick Start
// main.ts
import { Elysia } from "elysia";
import { buildPlugin } from "@byteholic/nelysia";
import { AppPlugin } from "./app.plugin";
new Elysia()
.use(buildPlugin(AppPlugin))
.listen(3000);Core Concepts
@Service — Injectable class
Register a class in the DI container. Pass deps as an explicit array.
import { Service } from "@byteholic/nelysia";
@Service()
export class UserRepo {
private users = [{ id: 1, name: "Alice" }];
findAll() { return this.users; }
}
@Service([UserRepo])
export class UsersService {
constructor(private repo: UserRepo) {}
all() { return this.repo.findAll(); }
}@Inject — Per-parameter injection
Use on constructor parameters instead of (or alongside) @Service([]).
import { Inject, Controller, Get } from "@byteholic/nelysia";
@Controller("/users")
export class UsersController {
constructor(@Inject(UsersService) private svc: UsersService) {}
@Get("/")
getAll() { return this.svc.all(); }
}Both styles are equivalent:
// Style A — class-level deps
@Service([UsersService])
@Controller("/users")
export class UsersController {
constructor(private svc: UsersService) {}
}
// Style B — per-param @Inject
@Controller("/users")
export class UsersController {
constructor(@Inject(UsersService) private svc: UsersService) {}
}@Plugin — Feature module
Composes services, controllers, sub-plugins, guards and hooks.
import { Plugin } from "@byteholic/nelysia";
@Plugin({
name: "users",
imports: [OtherPlugin], // sub-plugins (shared DI)
services: [UserRepo, UsersService],
controllers: [UsersController],
wsControllers: [UsersWsController], // WebSocket controllers
guard: {
beforeHandle: requireAuth, // applied to ALL routes
},
hooks: {
onRequest: ({ request }) =>
console.log(request.method, request.url),
onError: ({ error }) =>
console.error(error.message),
},
})
export class UsersPlugin {}Then in main.ts:
new Elysia().use(buildPlugin(UsersPlugin)).listen(3000);HTTP Routes
import { t } from "elysia";
import {
Controller, Get, Post, Delete,
Schema, BeforeHandle, Detail,
Body, Path, Query, Set,
} from "@byteholic/nelysia";
const adminOnly = ({ headers, status }: any) => {
if (headers["x-role"] !== "admin") return status(403);
};
@Controller("/users")
export class UsersController {
constructor(@Inject(UsersService) private svc: UsersService) {}
@Detail({ tags: ["Users"], summary: "List users" })
@Schema({ query: t.Object({ page: t.Optional(t.Numeric({ default: 1 })) }) })
@Get("/")
getAll(@Query() query: { page?: number }) {
return this.svc.all();
}
@Detail({ tags: ["Users"], summary: "Get user by id" })
@Schema({ params: t.Object({ id: t.Numeric() }) })
@Get("/:id")
getOne(@Path("id") id: number, @Set() set: any) {
const user = this.svc.byId(id);
if (!user) { set.status = 404; return { error: "Not found" }; }
return user;
}
@Detail({ tags: ["Users"], summary: "Create user" })
@Schema({ body: t.Object({ name: t.String({ minLength: 1 }) }) })
@Post("/")
create(@Body() body: { name: string }, @Set() set: any) {
set.status = 201;
return this.svc.create(body);
}
@BeforeHandle(adminOnly)
@Schema({ params: t.Object({ id: t.Numeric() }) })
@Delete("/:id")
remove(@Path("id") id: number) {
return this.svc.remove(id);
}
}WebSocket
import { t } from "elysia";
import { WsController, Ws, WsSchema, WsHandlers, Inject } from "@byteholic/nelysia";
@WsController()
export class ChatWsController {
constructor(@Inject(ChatService) private chat: ChatService) {}
@WsSchema({
body: t.Object({ user: t.String(), text: t.String() }),
params: t.Object({ room: t.String() }),
})
@Ws("/chat/:room")
chat(): WsHandlers {
const svc = this.chat;
return {
open(ws) {
ws.subscribe(ws.data.params.room);
ws.send("Welcome!");
},
message(ws, body) {
const msg = svc.format(body.user, body.text, ws.data.params.room);
svc.save(msg);
ws.publish(ws.data.params.room, JSON.stringify(msg));
},
close(ws) {
ws.unsubscribe(ws.data.params.room);
},
};
}
}
// Register in @Plugin:
@Plugin({
name: "chat",
services: [ChatService],
wsControllers: [ChatWsController],
})
export class ChatPlugin {}Connect with:
bun add -g wscat
wscat -c "ws://localhost:3000/chat/general"
# send: {"user":"Alice","text":"Hello!"}Context Decorators
| Decorator | Extracts |
|---|---|
| @Ctx() | Full Elysia context |
| @Body() | ctx.body |
| @Query() | ctx.query (all) |
| @Params() | ctx.params (all) |
| @Headers() | ctx.headers |
| @Cookie() | ctx.cookie |
| @Set() | ctx.set (status/headers) |
| @Path("id") | ctx.params.id (single) |
| @Q("page") | ctx.query.page (single) |
Lifecycle Hooks
Available in hooks: inside @Plugin:
| Hook | Fires when |
|---|---|
| onRequest | Every incoming request |
| onParse | Body parsing |
| onTransform | Before validation |
| onBeforeHandle | Before route handler |
| onAfterHandle | After route handler |
| onAfterResponse | After response is sent |
| onError | On any error |
| mapResponse | Transform response before send |
Macro
Wraps Elysia's native .macro() API:
import { Macro, MacroHandler } from "@byteholic/nelysia";
@Macro({ name: "auth" })
export class AuthMacro {
@MacroHandler("roles")
roles(required: string[]) {
return {
beforeHandle({ cookie, status }: any) {
if (!required.includes(cookie.session?.value?.role))
return status(403);
},
};
}
}
// Register in @Plugin:
@Plugin({ macros: [AuthMacro], controllers: [AdminController] })
export class AdminPlugin {}Full Decorator Reference
DI
| Decorator | Description |
|---|---|
| @Service(deps?) | Register class in DI container |
| @Inject(Token) | Per-parameter dependency injection |
Plugin
| Decorator / Function | Description |
|---|---|
| @Plugin(meta) | Define a feature module |
| buildPlugin(MyPlugin) | Convert to native Elysia instance |
HTTP
| Decorator | Description |
|---|---|
| @Controller(prefix) | HTTP controller class |
| @Get / @Post / @Put / @Patch / @Delete / @Options / @Head / @All | Route method |
| @Schema({ body, query, params, headers, response }) | Elysia t validation |
| @BeforeHandle(fn) | Per-route guard / hook |
| @AfterHandle(fn) | Per-route after hook |
| @OnError(fn) | Per-route error handler |
| @Detail({ tags, summary }) | OpenAPI metadata |
WebSocket
| Decorator | Description |
|---|---|
| @WsController(prefix?) | WebSocket controller class |
| @Ws(path?) | WebSocket endpoint (returns WsHandlers) |
| @WsSchema({ body, query, params }) | WS message validation |
Project Structure
src/
core/
container.ts — DI Container
service.ts — @Service, @Inject
controller.ts — @Controller, @Get, @Post, @Schema ...
context.ts — @Body, @Query, @Path, @Headers ...
websocket.ts — @WsController, @Ws, @WsSchema
plugin.ts — @Plugin, buildPlugin()
macro.ts — @Macro, @MacroHandler
index.ts — Public API barrelWith Swagger
bun add @elysiajs/swaggerimport { swagger } from "@elysiajs/swagger";
new Elysia()
.use(swagger({ documentation: { info: { title: "My API", version: "1.0.0" } } }))
.use(buildPlugin(AppPlugin))
.listen(3000);
// Docs at: http://localhost:3000/swaggerRequirements
License
MIT © byteholic
