Finally, an Object-Oriented Framework for Bun That Isn't a Monster
Source: Dev.to
Bun kicked the door down with absurd performance numbers. Everyone rushed to test Bun.serve(), and the first wave of frameworks (Elysia, Hono) focused heavily on a functional, minimalist, “bare‑metal” style.
They are amazing, don’t get me wrong. But I missed something.
Coming from ecosystems like .NET, Java, or even NestJS in Node, we get used to certain patterns: Dependency Injection (DI), Decorators, and Controllers organized into classes. I wanted that robust architecture without the weight of a framework that takes 5 seconds to boot. I wanted the structure of NestJS with the raw speed of Bun.
That’s when I found (and started using) Carno.js.
What is Carno.js?
Carno.js defines itself as a “performance‑first framework” native to Bun.
It doesn’t try to reinvent the wheel; it tries to organize it. Carno.js brings:
- Controllers & Decorators (
@Get,@Post,@Body, …) that you already know. - Dependency Injection that is native and lightweight.
- Custom ORM – a lightweight ORM with a custom SQL Builder (no Knex, no external query builders—just raw, optimized performance).
- True Modularity – the core is small. Install
@carno.js/ormor@carno.js/queueonly if you actually need them.
It’s for developers who want to build scalable, organized APIs without sacrificing Bun’s raw performance.
Installation & Requirements
-
Install Bun (v1.0+).
-
Create a folder and initialise the project:
mkdir my-carno-project cd my-carno-project bun init -
Install the framework core:
bun add @carno.js/core -
Enable decorators – Carno uses the classic decorator syntax, so update
tsconfig.json:{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true // ...other options } }
Creating a Project from Scratch (Step‑by‑Step)
1. Folder Structure
For a simple start, keep the layout minimal:
src/
├── controllers/
│ └── user.controller.ts
├── services/
│ └── user.service.ts
└── index.ts
2. Creating a Service (Dependency Injection)
Carno ships with a built‑in DI container—no external libraries needed.
// src/services/user.service.ts
import { Service } from '@carno.js/core';
@Service()
export class UserService {
private users = [
{ id: 1, name: 'Myron' },
{ id: 2, name: 'Bun User' }
];
findAll() {
return this users;
}
create(name: string) {
const newUser = { id: this.users.length + 1, name };
this.users.push(newUser);
return newUser;
}
}
3. Creating the Controller
If you’ve used NestJS or Spring, this will feel familiar. Notice the UserService injection in the constructor.
// src/controllers/user.controller.ts
import { Controller, Get, Post, Body } from '@carno.js/core';
import { UserService } from '../services/user.service';
@Controller('/users')
export class UserController {
constructor(private userService: UserService) {}
@Get()
getUsers() {
return this.userService.findAll();
}
@Post()
createUser(@Body('name') name: string) {
// Basic validation
if (!name) {
throw new Error('Name is required');
}
return this.userService.create(name);
}
}
4. The Entry Point
Tie everything together in index.ts. Carno needs to know which controllers and providers (services) you intend to use.
// src/index.ts
import { Carno } from '@carno.js/core';
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
const app = new Carno({
providers: [
UserService, // Register the service
UserController // Register the controller
]
});
const port = 3000; // Default port 3000 or whatever you pass
app.listen(port);
5. Running It
bun run src/index.ts
Done. No complex build step, no slow transpilation. It’s instant.
Full Example: Modularity
Carno supports nested routes (sub‑controllers). When your API grows, you don’t need a giant route file.
// Example of a versioned API controller
import { Controller } from '@carno.js/core';
import { UserController } from './controllers/user.controller';
import { ProductController } from './controllers/product.controller';
@Controller({
path: '/api/v1',
children: [UserController, ProductController]
})
export class ApiV1Controller {}
This makes API versioning painless.
Benefits and Trade‑offs
✅ Benefits
- DX (Developer Experience): Writing classes and decorators is very readable for large teams.
- Performance: Uses
Bun.serveunder the hood—fast. Very fast. - Integrated Ecosystem:
@carno.js/ormand@carno.js/queuefollow the same patterns, so you don’t have to stitch together different libraries manually.
⚠️ Trade‑offs
- Maturity: It’s newer than Express or NestJS. You might miss an obscure plugin you rely on in a legacy project.
- Bun‑Only: If your company policy forces Node.js, Carno isn’t (yet) an option.
- Community Size: Smaller than Hono’s or Elysia’s right now, so you may need to read the source code occasionally (which, honestly, is quite clean).
Practical Performance Tips in Carno
- Use Singleton: By default, services are singletons. Avoid
REQUESTscope (ProviderScope.REQUEST) unless strictly necessary (e.g., tenant data per request), as instantiating classes costs CPU. - JSON Serialization: Carno serializes objects automatically. Return plain objects (POJOs) in controllers instead of complex classes with circular methods.
- Cache: Carno has good cache integration. If an endpoint is heavy, cache it.
- Watch Your Logs: In production, use an async logger (Carno uses
pinointernally; configure it so it doesn’t block the main thread). - Avoid Heavy Middleware: Carno’s middleware model is flexible, but every middleware adds a “hop” to the request. Keep them lightweight.
Quick Comparison
| Framework | Style | When to Use? |
|---|---|---|
| Carno.js | OO, Decorators, Structured | You like NestJS/Java/.NET but want Bun’s speed and a custom, lightweight ORM. |
| Elysia / Hono | Functional, Minimalist | Ultra‑light microservices, edge functions, or if you prefer FP. |
| NestJS | OO, Opinionated, Massive | If you need a massive corporate ecosystem and run on Node.js. |
| Express | Legacy | Old projects. For new ones, avoid (slow and dated DX). |
Quick FAQ
1. Can I use it with SQL databases?
Yes! @carno.js/orm is the recommended way. It’s built specifically for this ecosystem and doesn’t carry the baggage of older query builders.
2. Does it support Validation?
Yes, it uses class-validator and class-transformer natively. You create DTOs with decorators like @IsString().
3. How does it work with Testing?
Bun has a native test runner (bun test). Since Carno uses dependency injection, it is trivial to “mock” your services in controller unit tests.
4. Is it compatible with Node libraries?
Mostly yes, thanks to Bun’s compatibility. Prefer libraries that don’t rely on obscure native Node APIs.
5. Can I use it in Serverless?
You can, but Carno shines brighter in containers/long‑running processes where the DI and structure pay off.
Conclusion
Carno.js fills an important gap: robust structure without slowness. If you were waiting for an excuse to migrate that slow Node API to Bun but were afraid of losing your code organization, give Carno a shot.
Useful links
Liked the tip? Have you tried any OO frameworks in Bun? Let me know in the comments!