Building KickJS for the world
Published
kickjs, nodejs, open-source, africa
Published
kickjs, nodejs, open-source, africa
KickJS is a decorator-driven Node.js framework for TypeScript. Today it is 17 npm packages, runs on Express, Fastify or h3, and deploys to Node, Bun, Deno and Cloudflare Workers. It did not start that way, and it did not stay the biggest it ever was. This is the story of how it grew, why I deliberately made it smaller, and why it matters to me that a Kenyan engineer is building a layer other developers install.
KickJS started as a personal scaffolding tool. I kept setting up the same kind of API project: the same folder structure, the same wiring for configuration, validation and routing. A generator that did that in seconds was the first version.
The repository's first commit is from 20 September 2025, and the first releases, 0.1.3 and 0.1.4, went out the same day. A month later, on 29 October, the commit history says "Add base infrastructure core for Kickjs". That is the point where a scaffolding tool started turning into a framework.
The frustration behind it was with the established frameworks. NestJS and platforms like it come with strong opinions about how things are done, a lot of boilerplate, and a lot happening behind the scenes. That makes it hard to understand what your own application is actually doing. Django, for all its structure, had its own black box for me: metaclasses. Every Django model is built by a metaclass, ModelBase, which turns a plain class definition into fields, managers and database mappings behind the scenes. When I was still learning Django, that was hard to understand, and the advice I found online pointed the other way. The highest-voted Stack Overflow answer on metaclasses ends by saying they are complicated: 99% of the time you need to alter a class, you are better off with monkey patching or class decorators, and 98% of the time you don't need class alteration at all. That was exactly what bothered me: the part doing the work was the part I was told I could ignore.
I wanted the opposite: transparency. A mathematical function takes an input and returns an output, and you can read the processing in between. I wanted a framework to work the same way: this is where the request comes in, this is where the response goes out, and the logic between them is plain code, not magic.
Structure was the other half of the problem. When I moved to large-scale software, Express's primitives started to hurt. Express describes itself as an unopinionated, minimalist web framework, so there is no set way to structure an application: folders drift and every team writes code differently. Many of us learn through apprenticeship or YouTube videos, and we write software the way we saw someone else write it. Spring Boot and Django are different. Django's startapp gives every app the same files, from models.py and views.py to a migrations folder, and Spring Boot's documentation recommends a root package with controllers, services and repositories grouped by feature. Spring Boot's structure has a cost, though: it leans on annotations, and there are a great many of them to learn before the pieces make sense, from @SpringBootApplication and @ComponentScan to @RestController, @Service, @Repository, @Autowired, @Configuration and @Bean. NestJS brings an out-of-the-box architecture to Node.js, heavily inspired by Angular, but it felt heavy. I wanted something lighter.
Part of that weight was how NestJS modules work. Every module lists its imports, controllers, providers and exports, and a provider only becomes visible to another module after its host module exports it and the consuming module imports it. Configurable modules add another layer of conventions, register, forRoot and forFeature, each with an async version. I kept asking why the framework couldn't resolve those dependencies itself, instead of making me pack so much into a single module definition. In KickJS, a class marked @Service() registers itself with the dependency injection container, and a module only binds interfaces to implementations when you need that, alongside declaring its routes.
Then there was the setup. Before writing any real code I would wire up about five tools: path aliases, a correctly configured TypeScript compiler, a build tool that transpiles while still letting me type-check, and a dependency injection library, where I went back and forth between options like tsyringe and InversifyJS. Putting all of that inside KickJS, so you don't install thirty packages before you start, became one of its main goals.
Feedback speed mattered most. KickJS is built on Vite, which reloads a TypeScript change in milliseconds. Other setups I tried, transpiling and then restarting, could take several seconds or even a minute. If every save costs you thirty seconds, those seconds eat a real part of an eight-hour day. On the frontend, Vite is close to overtaking webpack as the most-used build tool, according to the State of JavaScript 2025 survey, and I wanted the backend to get the same immediate feedback when you save, whatever the size of the codebase.
The goal became a framework that sits between Express and NestJS: the structure of decorators, modules and dependency injection, without giving up control of what runs underneath.
A controller in KickJS doesn't know which HTTP engine it runs on. It speaks a request context, and the engine is chosen once, at bootstrap (the HTTP runtimes guide explains how):
import { bootstrap } from '@forinda/kickjs'
import { fastifyRuntime } from '@forinda/kickjs/fastify'
// Express is the default. Fastify is one option away, with the same modules and controllers.
export const app = await bootstrap({ modules, runtime: fastifyRuntime() })Around that core came the pieces a production API needs: a CLI that scaffolds projects and modules, Vite-powered hot reload, type generation so route params and responses are typed end to end, a typed fetch client for the frontend, OpenAPI generation from Zod schemas, and an edge entry point that runs the same app as a fetch handler on Cloudflare Workers, Bun and Deno.
At its widest, KickJS also shipped packages for authentication, cron jobs, email, OpenTelemetry, GraphQL, multi-tenancy, notifications, and adapters for Prisma and Drizzle. Twelve of those packages are now deprecated on npm.
They fall into three groups:
@forinda/kickjs-core, @forinda/kickjs-http and @forinda/kickjs-config became part of @forinda/kickjs. Each got a final release telling you what to import instead.auth, cron, mailer, otel, graphql, multi-tenant and notifications stopped being packages. Each has a guide showing how to build the same thing in your project.@forinda/kickjs-db took their place.The reasoning is written into the KickJS docs. Domain layers like auth, multi-tenancy, audit and observability vary from project to project: different claim shapes, different session stores, different policy rules. When a framework ships an opinionated package for one of those, two things happen. Teams with edge cases either fork it or ask for features that grow the package for everyone. And the framework's release cycle gets tied to that domain, so upgrading the framework forces you to review your auth.
For me the moment was concrete. I bootstrapped an application that needed third-party authentication, and with the auth package everything was already wired up: plugins for every piece, setup done, all of it working. Then I realised how much magic that was. If someone wanted to extend it in a way I hadn't planned for, they would have to come to me, the framework maintainer, and ask for the feature.
That changed who I was building for. KickJS was no longer only for my own projects. Other developers would bring opinions that don't match mine, and the framework should give them room to express those opinions instead of making them sit on mine.
Databases made the same point. In the Python world, the two most popular ORMs mirror the two leading web frameworks, and your framework mostly picks for you: Flask projects reach for SQLAlchemy through Flask-SQLAlchemy, and Django projects use the Django ORM with its built-in models and migrations. I had started down a similar road, building scaffolds around Prisma and Drizzle. Then Prisma 7 came out in November 2025 with breaking changes, from a new client generator and a required config file to mandatory driver adapters, and it broke most of what I had set up. I sat down and asked myself whether I would keep maintaining other projects' breaking changes inside my framework. The answer was no. I would build the layers and the ways to use them, and let people choose the tools they want, without fighting the framework to do it.
KickJS keeps shipping the primitives those packages were built from: defineAdapter, definePlugin, defineContextDecorator and the dependency injection container. An adapter that used to be a package becomes a few lines in your own code:
import { defineAdapter } from '@forinda/kickjs'
import { AUTH_STRATEGIES, LoadAuthUser } from './load-auth-user'
import type { AuthStrategy } from './strategies'
export interface AuthAdapterOptions {
strategies: readonly AuthStrategy[]
defaultPolicy: 'protected' | 'open'
}
export const AuthAdapter = defineAdapter<AuthAdapterOptions>({
name: 'AuthAdapter',
register: (container, opts) => {
container.registerInstance(AUTH_STRATEGIES, opts.strategies)
},
contributors: (opts) =>
opts.defaultPolicy === 'protected'
? [LoadAuthUser.with({ on401: 'reject' }).registration]
: [LoadAuthUser.with({ on401: 'allow' }).registration],
})There is a real trade-off here. The auth guide says it plainly: each project now owns roughly 200 lines of auth code, and the framework owns only the primitives. In exchange, your auth changes when your product needs it to, not when KickJS releases.
When I started, KickJS was just primitives, and I wasn't sure yet how they would hold up. I was also learning how to roll out a project of this size. I didn't know whether anyone else was using it, but I was building side projects on it myself, and those projects showed me the issues.
Releases were their own problem. For a long stretch I was shipping major version after major version. It was around version 3 that plugins and adapters arrived, and with them the philosophy KickJS still follows: an adaptable framework that conforms to your thinking and your opinions, not mine.
That philosophy shaped how I handled the breaking change to authentication. Instead of removing the package and leaving people to rebuild it, I wrote bring-your-own implementations. They show how KickJS did auth, expressed in your own code. Moving to the next major release then means refactoring only the pieces that need it, so the application keeps working the way it did before.
Every deprecated package also says where to go next. The npm notices for the merged packages say what to import from @forinda/kickjs instead and mark the final release. The notices for the recipe packages point to the guide that replaces them. The docs describe these migrations as one-way and additive: you move code into your project, and you don't lose behaviour.
Much of my motivation came from developers whose tools and communities reach millions of people. Evan You created Vue and then Vite. Anthony Fu created Vitest, VueUse, UnoCSS and Slidev, and is a core team member of Vue and Vite. Taylor Otwell built Laravel and the community around it. Seeing what they made showed me how far one developer's work can travel.
There are many JavaScript frameworks, but none of them ticked every box for what I wanted to achieve. Before KickJS I had thought about building an ORM, even a database engine, but those stayed ideas. The framework was the thing I actually needed.
I also noticed something missing. In my entire time as a developer I haven't come across a major framework built in Africa. Maybe some exist and never made the news. The closest I know of is Nuru, a programming language written in Swahili. It went public in December 2022 to teach programming and computational thinking to Tanzanian students in their own language, and it has since grown to more than 25 contributors from around East Africa. It stayed with me because it shows what building from here, for people here, can look like.
We have great developers and great minds in Africa. I'll speak only for Kenya, and only for what I've seen, not on behalf of every developer. Many of the strongest engineers I know are aiming at the corporate market in Europe or America, and it's easy to see why: better compensation, better incentives, and more recognition for the skills and value they bring.
Building world-class software from here runs into real limits. The first is resources and funding. The second is infrastructure. Setting up a data center in Kenya is hard when electricity rationing still happens, because a server that loses power during rationing is a server that goes down. Solar can fill the gap, but installing it is still expensive.
Strides are being made to close those gaps. I believe we're not far off, but we're not there yet, and individuals can't close the gap alone. It needs government policy that favours the development and growth of software, so that what we build can serve a global scale.
Talent is another challenge. The people who are really good get hired by the tech giants with better compensation. They go on to serve those companies, and sometimes leave behind bigger things they might have built if they had stayed.
Established players make the market hard to enter, too. A company that already dominates can see a newcomer's idea, buy it or build its own version, and claim it. The newcomer's work never gets its chance out there. That brings it back to policy again, and to the competitive advantage a new builder needs against giants already in the market.
I want to contribute to the software ecosystem as an African, and show that Africans can build products that enterprises depend on. KickJS, fcms, fordb and the Forinda RTC SDK are my attempt at that: tools other developers build on, made by a Kenyan engineer for developers anywhere. Nobody installing @forinda/kickjs needs to know where it was written, and that is the point.
Everything in this article is open source. If one of these projects is useful to you, a star on GitHub helps other developers find it.
The rest of my work is on GitHub.