Skip to main content

Command Palette

Search for a command to run...

Building a backend without giving your architecture to a framework

Quilla Backend Kit provides the foundations for structured TypeScript services while leaving the application architecture in your hands.

Updated
6 min readView as Markdown
Building a backend without giving your architecture to a framework
M

I'm an enthusiastic Chilean software engineer in New Zeland. I mostly focus on the back-end of the systems. This is my site, Señor Developer, where I share my knowledge and experience.

Most backend services don’t begin with complicated architecture.

They begin with an HTTP endpoint, some application logic, and a database call. Then requirements accumulate: authentication, transactions, background jobs, domain events, structured logging, graceful shutdown, and perhaps tenant isolation.

Eventually, the application needs more structure. The usual choices are to adopt a comprehensive framework or build the missing foundations yourself.

Both can work. But there’s a useful space between them.

That’s the space I’m exploring with Quilla Backend Kit.

A toolkit, not an application framework

Quilla Backend Kit is a collection of TypeScript packages for building production Node.js services with explicit domain logic and without full-framework lock-in.

It provides foundations for:

  • Domain-driven design

  • Execution context

  • Persistence and transactions

  • Domain events and messaging

  • HTTP controllers

  • Authentication and security

  • Structured observability

  • Background jobs and graceful shutdown

These capabilities are separated into focused packages. You can adopt the parts your service needs without placing the whole application inside a framework-owned architecture.

This distinction matters.

A framework often gives you an application model: its modules, dependency injection system, decorators, lifecycle, and preferred way of connecting everything. Quilla Backend Kit provides smaller architectural primitives while leaving the composition of the application in your hands.

There’s no generated project structure, mandatory container, or hidden runtime.

The idea behind Quilla

Quilla is the Spanish word for a ship’s keel: the structural backbone to which the rest of the vessel is attached.

That’s the role I want the toolkit to play. It provides the load-bearing parts of a backend service without trying to become the entire service.

The domain still belongs to the application. So do its business rules, module boundaries, infrastructure choices, and composition root.

The toolkit supports that architecture rather than replacing it.

What does that look like in practice?

Consider an application service that needs to create a user and publish a domain event.

The aggregate owns the business operation. The repository handles persistence, while the unit of work ensures that the aggregate and its outgoing event are committed together:

await unitOfWork.transaction(async (transaction) => {
  const user = User.create({
    email,
    passwordHash,
  });

  await userRepository.save(user, transaction);
});

Creating the user can produce a domain event inside the aggregate:

export class User extends AggregateRoot<UserProps> {
  static create(props: CreateUserProps): User {
    const user = new User({
      ...props,
      id: crypto.randomUUID(),
    });

    user.addDomainEvent({
      type: 'user.created',
      payload: {
        userId: user.id,
      },
    });

    return user;
  }
}

When persistence and messaging are configured together, the aggregate state and its outbox entry are committed in the same transaction. A background worker can then publish the event for another part of the system to process:

export class OnUserCreatedHandler
  implements EventSubscription<UserCreatedPayload>
{
  readonly descriptor = UserCreatedEvent;

  constructor(private readonly logger: Logger) {}

  async handle(entry: HandlerEntry<UserCreatedPayload>): Promise<void> {
    this.logger
      .forMethod('onUserCreated')
      .withMeta({ userId: entry.payload.userId })
      .info('User created');
  }
}

The correlation information from the original request can travel through the transaction, outbox, and event handler using Quilla’s execution context.

These are intentionally separate pieces. Your application decides how they are assembled and where its architectural boundaries belong.

What makes it different?

One of the project’s central principles is a strict separation between interfaces and adapters.

The core packages define concepts such as repositories, units of work, logging, authentication, and messaging without requiring a particular database or HTTP framework. Concrete integrations are provided separately, including adapters for PostgreSQL and Hono.

This makes the dependencies visible.

If your application uses PostgreSQL, it opts into PostgreSQL. If it uses Hono, it opts into Hono. Those decisions don’t need to spread through the domain or application layers.

Quilla Backend Kit also treats concerns such as scope isolation, audit information, correlation IDs, and durable domain events as architectural foundations rather than details to add later.

You don’t need to adopt everything

A practical way to introduce the toolkit is to begin with the pressure already present in your project.

If your domain model is becoming difficult to express, start with the DDD primitives:

pnpm add @quilla-be-kit/ddd @quilla-be-kit/errors

If request and event processing need consistent identity and correlation information, add execution context and observability:

pnpm add @quilla-be-kit/execution-context @quilla-be-kit/observability

For transactional persistence and durable event delivery:

pnpm add @quilla-be-kit/persistence @quilla-be-kit/messaging pg

Or, when creating an HTTP service:

pnpm add @quilla-be-kit/http @quilla-be-kit/security hono @hono/node-server

Each package can be introduced around a real need. You don’t have to rewrite the application or adopt an entire stack at once.

Keep the toolkit behind your application’s language

Quilla Backend Kit uses deliberately generic terms such as scopeId. Your application decides whether that scope represents a tenant, organisation, workspace, or project.

I recommend placing a small application-owned layer around the toolkit. That layer can translate generic concepts into the vocabulary of your domain.

For example, your application might expose TenantId and TenantRepository, while using Quilla’s scoped persistence underneath. Application code then speaks in domain terms rather than toolkit terms.

This also creates a useful boundary: if an infrastructure choice changes, most of the application remains unaffected.

Who is it for?

Quilla Backend Kit is intended for engineers building TypeScript services who want architectural foundations without adopting an all-encompassing framework.

It may be useful if you are:

  • Building a multi-tenant or event-driven service

  • Applying domain-driven design

  • Implementing a transactional outbox

  • Separating persistence interfaces from database adapters

  • Rebuilding the same execution-context or logging foundations across services

  • Looking for more structure than a minimal HTTP framework provides

It probably isn’t the right choice if you want a generated application, extensive conventions, or a framework that makes most architectural decisions for you.

That’s a deliberate trade-off. Quilla offers ownership and composability, but ownership also means making the final decisions yourself.

Where the project stands

Quilla Backend Kit is open source, MIT-licensed, ESM-only, and designed for Node.js 22 or later.

It is currently pre-1.0, so APIs may change between minor releases. I’d rather make that explicit while the project’s public contracts continue to mature.

If the approach sounds relevant to the way you build services, explore the packages and their detailed documentation in the repository:

Explore Quilla Backend Kit on GitHub →