Tuesday, April 23rd 2024

Basica: Yet another Javascript Backend Framework

TL;DR

I’m working on a new backend framework, Basica. Check it out, you’ll hopefully find it useful!

The state of backend frameworks

Doing backend in the JS ecosystem is an emotional rollercoaster. Every time you work on something new, whether it’s on another company, another team, another project, even the next major rewrite of the project you are working on, it feels like you are learning a whole new language again. You stumble on a framework you have never seen before, new “patterns”, libraries and testing suites. Slowly you begin to make sense of what your eyes are seeing. After a couple hours (or days) of struggle, you are productive again, ready for the incumbent context switch to throw all your progress away.

I’ve worked with other technologies other than Javascript, and I will say that the problem is very exaggerated in this ecosystem. Perhaps the ever growing number of packages, today over 2 million, has something to do with it. Perhaps dynamic typing makes people very creative with how they structure their code. Maybe it’s the mistakes Node.js has introduced in the past.

In the end Javascript code is all over the place. In C# and ASP.NET for instance, all the wiring happens in Program.cs, the rest of the codebase is pretty boring and well structured. Something like this allows you to context switch like a maniac, if there’s just one spot per project where things may be change, you can move very fast. But as you can’t lock a wild animal in a cage, you can’t expect that every JS dev would be willing to follow a framework’s structure, so you must plan accordingly and come up with a solution that’s flexible enough.

Another pain point is that libraries are all very different, integrating common functionality, like healthchecks, is usually done each time you bootstrap a new project, usually each time in a different way. Since there’s no “official” framework like ASP.NET, no one ever bothered adding the common funcionality. It wouldn’t make sense to grow a package size and complexity by adding code that will only be used used in a fraction of all projects. With that said, if we ever need to implement something like this, it should likely be done in a given framework’s codebase.

There are frameworks like NestJS that tried to solve these problems, but in my opinion they all failed:

  • Name one Javascript backend framework
  • Is it still maintained?
  • Was it flexible enough (did you have to rewrite most of your old code)?
  • Did it have any missing features you would have wanted (and couldn’t integrate them by yourself)?
  • Was it performant enough?
  • Was is far too much complicated?
  • Would you use it again?

If you are one the guys that would use NestJS again, please read this, this, look at this and perhaps think again.

Basica

Since I don’t resonate with any of the current standards, I think it’s time for a new standard!

standards

Seriously though, I hope my ideas resonate well in the ecosystem, allowing for a better development experience, an overral focus on software quality and perhaps a lending hand on the project.

Introducing today, Basica.

In a nutshell, it should feel like a mix of building everything by yourself and using a fully-fledged framework and your application entrypoint should look like ASP.NET’s Program.cs.

Basica should not hinder your freedom, and you are free to build applications however you want.

Configuration parsing, startup, shutdown, healthcheck probes and all those kind of repetitive tasks should be managed by Basica.

To avoid developers the burden of said tasks, Basica should interface with popular libraries through plugins.

Dependency injection should not be rocket science, in Basica it’s not.

Basica should provide observability support, such that you can understand why the hell your service suddenly exploded in prod and then channel your anger on me in a GitHub issue.

Your index.ts should look like this:

import { IocContainer } from "@basica/core/ioc";
import { loggerFactory } from "@basica/core/logger";
import { AppBuilder } from "@basica/core";
import { configure, envProvider } from "@basica/config";

import { lifecyclePlugin } from "@basica/fastify";

import { Type } from "@sinclair/typebox";

// Validate configuration
const config = configure(envProvider(), Type.Object({
  logger: loggerConfigSchema
}));

// Dependency injection
const container = new IocContainer()
  .addSingleton("logger", () => loggerFactory(config.logger))
  .addSingleton("svc", (s) => ({
    hello: () => {
      s.logger.info("svc called!");
      return "hello world";
    },
    healthcheck: () => ({ status: "healthy" }),
  }));

const app = new AppBuilder(container)
  // Lifecycle management
  .configureLifecycle((b, c) => b
    // Healthchecks
    .addHealthcheck("svc", (c) => c.svc)
    // Plugins
    .with(lifecyclePlugin, (b) => b
      .addFastifyEntrypoint("http", (f) => f
        .mapHealthchecks({ path: "/health" })
        .configureApp((app) => {
          app
            .useOpenapi()
            .fastify.get("/", () => c.svc.hello());
          }
        )
      )
    )
  ).build();

app.run();