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

Published: (January 1, 2026 at 03:06 PM EST)
5 min read
Source: Dev.to

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

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

Installation & Requirements

  1. Install Bun (v1.0+).

  2. Create a folder and initialise the project:

    mkdir my-carno-project
    cd my-carno-project
    bun init
  3. Install the framework core:

    bun add @carno.js/core
  4. 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.serve under the hood—fast. Very fast.
  • 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

  • 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 REQUEST scope (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 pino internally; 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

FrameworkStyleWhen to Use?
Carno.jsOO, Decorators, StructuredYou like NestJS/Java/.NET but want Bun’s speed and a custom, lightweight ORM.
Elysia / HonoFunctional, MinimalistUltra‑light microservices, edge functions, or if you prefer FP.
NestJSOO, Opinionated, MassiveIf you need a massive corporate ecosystem and run on Node.js.
ExpressLegacyOld 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!

Back to Blog

Related posts

Read more »

Stop Writing APIs Like It's 2015

We're in 2025, and many codebases still treat APIs as simple “endpoints that return JSON.” If your API design hasn’t evolved past basic CRUD routes, you’re sacr...