DEV Community

Myron Nevzorov
Myron Nevzorov

Posted on

Finally, an Object-Oriented Framework for Bun That Isn't a Monster

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 but 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. It 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. You only install @carno.js/orm or @carno.js/queue if you actually need them.

It's for developers who want to build scalable and organized APIs without sacrificing Bun's raw performance.

Installation & Requirements

First, you need Bun installed (v1.0+).

Create a folder and initialize the project:

mkdir my-carno-project
cd my-carno-project
bun init
Enter fullscreen mode Exit fullscreen mode

Now, install the framework core:

bun add @carno.js/core
Enter fullscreen mode Exit fullscreen mode

Warning: Since Carno uses decorators (the classic way), you must adjust your tsconfig.json. This is mandatory:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
    // ...other options
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating a Project from Scratch (Step-by-Step)

Let's build a simple User API to show how the OO architecture shines here.

1. Folder Structure

I like to organize by domain, but for starters, let's keep it simple:

src/
├── controllers/
│   └── user.controller.ts
├── services/
│   └── user.service.ts
└── index.ts
Enter fullscreen mode Exit fullscreen mode

2. Creating a Service (Dependency Injection)

Carno has 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Creating the Controller

Here is where the magic happens. If you've used NestJS or Spring, you'll feel right at home. 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

4. The Entry Point

Now we 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
  ]
});

// Default port 3000 or whatever you pass
const port = 3000;

app.listen(port);
Enter fullscreen mode Exit fullscreen mode

5. Running It

bun run src/index.ts
Enter fullscreen mode Exit fullscreen mode

Done. No complex build step, no slow transpilation. It's instant.

Full Example: Modularity

One cool thing about Carno is that it supports nested routes (sub-controllers). If your API grows, you don't need a giant route file.

@Controller({
  path: '/api/v1',
  children: [UserController, ProductController]
})
export class ApiV1Controller {}
Enter fullscreen mode Exit fullscreen mode

This helps a lot with API versioning without the headache.

Benefits and Trade-offs

Not everything is perfect; let's be honest.

✅ Benefits

  1. DX (Developer Experience): Writing classes and decorators is very readable for large teams.
  2. Performance: It uses Bun.serve under the hood. It's fast. Very fast.
  3. Integrated Ecosystem: @carno.js/orm and @carno.js/queue follow the same patterns, so you don't have to stitch together different libraries manually.

⚠️ Trade-offs

  1. Maturity: It's a newer project compared to Express or NestJS. You might miss that one obscure plugin you use in a legacy project.
  2. Bun Only: If you are forced to use Node.js by company policy, Carno is not (yet) for you.
  3. Community: Smaller than Hono's or Elysia's right now, so you might have to read the source code occasionally (which, honestly, is quite clean in Carno).

Practical Performance Tips in Carno

  1. Use Singleton: By default, services are singletons. Avoid REQUEST scope (ProviderScope.REQUEST) unless strictly necessary (e.g., tenant data per request), as instantiating classes costs CPU.
  2. JSON Serialization: Carno serializes objects automatically. Return plain objects (POJOs) in controllers instead of complex classes with circular methods.
  3. Cache: Carno has good cache integration. If an endpoint is heavy, cache it.
  4. Watch your Logs: In production, use an async logger (Carno uses pino internally; configure it so it doesn't block the main thread).
  5. 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. But 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!

Top comments (0)