Creating my own framework
If you have read some of my other blog articles, you might have spotted the pattern of me making up problems to justify reinventing the wheel using Node.js (and Vue), and this is certainly no exception.
Before going into more detail about the framework itself, I want to focus more on the why, which I think is way more interesting; the framework itself is quite boring.
The Joy of Stupid Ideas
I felt the urge to build a custom framework because I love turning stupid ideas into real apps. And I mean this in a “the journey is the goal” kind of way. I enjoy coming up with an idea and jumping straight into it, coding through the night.
Unlike coding for work, my hobby projects don't need to make sense and don't need to be created as fast and efficient as possible - they just need to be fun. This means the end result doesn't even matter. Most of these projects never get hosted anywhere for anyone (not even me), and that is totally fine.
But modern frameworks have started making the initial sprint much harder than it needs to be.
Why Laravel Lost Its Magic for Me
When coding a hobby app, I no longer use Laravel because I end up spending a whole evening installing the framework and then ripping out or rewriting the entire frontend just because I don't want to use Tailwind or Typescript but I also don't want to skip starter kits, as they provide sensible defaults and teach me the current best practices for matching a frontend with the latest backend version.

Sure, I can force Laravel to accept my way of things, but Laravel makes sure you feel bad while doing so. Because it is so opinionated, everything works smoothly only as long as you adhere to its defaults. If you go your own way, you feel like you are missing out - or like you're purposefully ignoring hours of thoughtful engineering.
But this “opinionated way” of Laravel seems to change with every version.
Every time I open the “installation” page in their docs, there is something different I have to read through, assess, and decide if it fits my development environment. Should I use composer global require laravel/installer ? Should I use "Laravel Herd, the fully-featured graphical PHP installation and management experience"? Is this for windows only or will this work on linux while using SSH remote development? In the past, Laravel Sail (docker based) was advertised as the best thing ever; now, it is mentioned nowhere. The first thing you read now is Laravel suggesting that you switch off your brain and let an AI slopcode it for you.
PHP is still the best language for production web apps in my opinion, but it also needs - I don't want to say "complex" - but it does require some setup. Which is why I created my own Docker images a few years ago to include:
- PHP-FPM with all necessary extensions and optimized config
- Nginx pointing to the php socket
- Supervisor for running Nginx, the Laravel worker, and Laravel crons
- Xdebug
- Correct permission handling
just to name a few.
By the time the project was finally in a clean state where it felt like I could start implementing my idea, it was 3 o'clock in the morning on a workday. And the laravel project suddenly felt "dirty" because I straved too far from the default.
If you read through all this complaining and think, “Why not use AI? It would make these apps production-ready and feature-complete in no time,” then I suggest you reread the beginning. This is not the point.
Why do people ride bikes when they could cover the distance way easier and faster with a car? Because they enjoy the exercise and the ride.
What "baseline" attempts to fix
baseline was planned to be something similar to pocketbase, but not a clone. It should be something I can install and start creating database entities and writing Vue components that interact with said entities, spending as little time as possible with setup without compromising on "production-a-bility".
To be honest, one of the main reasons was that I wanted a framework that would lower the barrier to entry enough so that I wouldn’t have to be afraid of simply giving an idea a go, because then I wouldn’t have to feel bad afterwards about having spent too much time on boilerplate before the fun even started.
Intentional development
Excerpt from the "Philosophy" page of the baseline docs:
Great development happens when you are "in the flow" - not stuck wrestling with complex infrastructure or endless configuration files. Baseline is built to bridge the gap between side projects and professional-grade software. It provides the high standards required for real-world applications without the soul-crushing overhead typical of enterprise toolchains.
I believe that coding should be enjoyable. In an era of rapid, low-effort automation, baseline encourages intentional development by keeping your focus on application logic and meaningful features rather than managing boilerplate.
The term Intentional development is my concept of a mindset that brings craft and joy back to software engineering. It is the conscious choice to stop treating code as disposable output rather than something to be understood and built with care.
Lots of decisions
I chose Node.js as the language because it somehow has established itself as my "feel-good" language. Next up, I had to decide which libraries fit my style.
- Which database
- Which ORM
- Which web framework
- Which authentication
- Which frontend tooling
All of these decisions were based on my experience over the years. Each library or tool chosen had to be actively maintained and proven to be robust, intuitive, and easy to understand with great DX and minimal configuration.
You can read more about the design choices in the docs.
None of these decisions are perfect and come with their own drawbacks. For example, Fastify is a more performant alternative to Express, which brings easy integration with schema validation and more, but I chose Express over Fastify because it is more stable and I had issues with Fastify regarding WebSockets and Vite. Most of the time, I just went with what felt right because ultimately this is the goal: a framework that feels right to me.
Design Choices
Why I chose a library/tool or not:
Why SPA?
I chose Vue.js for the frontend - so you either have to use SSR or communicate with the backend using an API.
SSR is bullshit in my opinion.
- You have to manage you frontend framework on the server and the client.
- SSR is better for SEO but baseline apps are not thought to be indexed since everything is behind authentication.
- SSR requires a way more complex setup and integration, plus you have to write your Vue Components "SSR friendly"
- SSR is basically the result of a full circle
- The web started as fully server side rendered web pages
- Then javascript allowed creating more interactive websites
- Suddenly everyone was doing fully client-side rendered JS based SPAs, this caused websites to be clientside-heavy and slow and for a long time, the Google bot did not execute javascript resulting in bad SEO
- SSR was invented to fix these issues and we are back to server side rendered web pages - Vue has become a backend templating engine. wtf.
Okay so SPA it is, we require an API and generating it automatically from the database schema saves a lot of time so you do not need to create all the create/update/delete endpoints yourself.
This way you can jump straight into building the app instead of switching between frontend and backend all the time.
Why MySQL?
I wanted to use PostgreSQL because it seems like the "first class" database supported by drizzle. In general it has many cool features that mysql does not have, even if it is just something simple as "insert ... returning".
So why not?
- The PostgreSQL docker image
- The user defined via env is a superuser which can lead to stuff working locally but breaking as soon as you run it against a real db using a non-superuser
- Upgrading a PostgreSQL database is a pain in the ass
- PostgreSQL adds an additional nesting level, the "schema"
- Drizzle creates a separate schema for the migrations which requires elevated permissions a normal database user should not have.
- I like taking a peak at the data in the database and even modify it or run SQL statements to debug stuff, for this I need a client
- All VSCode database extensions suck in my opinion
- A docker container that spins up beside the database like phpmyadmin exists, pgadmin, but pgadmin is so much more complicated, you have to click like 20 times to reach the data of a table, setup is more complex too
- A database client that runs locally on my machine like beekeeper studio needs to reach the database somehow. If it runs inside WSL or on my local dev server I need to configure networking like SSH tunneling and map the port on the host or more to actually connect.
- While everything by drizzle is opensource, drizzle-studio is not.
- MySQL might not be the "coolest" database but it works.
Why drizzle
Drizzle allows schema definition using js functions which is important for me to hook into the entity definitions.
- Prisma requires a custom language for the schema, extending it is more complicated.
- Prisma also requires generating a heavy typescript client. While this offers type-safety and autocomplete, I do not use typescript and regenerating the client each time you change something is annoying.
- Drizzle offers integration with zod for schema validation
- The drizzle RQB object style fits my api idea very well
- Drizzle is not as invasive as other ORMs regarding the project folder structure
Why express
Same as MySQL, it just works. The ecosystem is mature and predictable.
- Fastify is very nice but it is still evolving, some "core" features are driven by a single dev or have not been updated in a while
- At one point the official documentation page pointed to a crypto scam website for months?
- I had immense issues getting fastify/websocket to play along nicely with fastify/vite because fastify basically intersects all websocket connections.
I do not want to list more, you get the point. It all breaks down to a mixture of
- what feels right to me
- has good developer experience
- as little friction as possible
- is still somewhat professional / widely used and maintained
Documentation struggles
I do not enjoy writing documentation when it feels like it is slowing down development. But you cannot make a framework so simple that it is self-explanatory without leaving out a lot of features.
My idea to lower the pain was trying to use a local LLM to help me with grammar and phrasing. This "quick helper task" spiraled into hours and hours of writing a tool to improve the documentation automatically. First only spell checking, then enforcing a writing style, then finding possible cross-links and inconsistencies using a vector database of the docs and source code. First as a CLI tool, then a VS Code extension, then a standalone tool.
It was fun because it was a little side project inside a side project but it was also very frustrating because the local LLM context is limited by my available VRAM; with limited VRAM, you can choose between a good LLM that is slow and has little to no context or a fast LLM that is stupid but has big context. It would often get stuck in a reasoning loop - it would only find 1 or 2 suggestions and they were often unnecessary and different every time.
Learning #1 - Local LLMs are great for spell checking and rephrasing, but they will not magically write the documentation for me. I was basically "vibe-documenting" and then wondering why the documentation is bad.
Learning #2 - I should have waited to write the docs a bit. I was still actively changing major things of the framework which means adjusting the docs every time.
Learning #3 - Too many cross-links make the docs feel like a labyrinth; a good menu structure is more important.
Learning #4 - I do like to share my thoughts and explain (or maybe even defend) my choices, but documentation is the wrong place for that. I kept a bit in the docs, but I do not like how the LLM rephrased my words into professionalized office talk. When I write “Tailwind sucks ass,” I mean it - not “Tailwind did not fit my needs.”
In the end, a developer mostly does not care why the framework is how it is or why it uses certain libraries or not because if they want to use the framework they have to accept its choices. So again, the docs are the wrong place for explaining and defending my choices, so I did it here instead.
Local npm package struggles
This problem has been haunting me for a few years now and was the topic of my seconds blog post.


For this project I tried again and I was able to find a solution that works okay-ish.
npm workspaces
I have a folder that contains all baseline repos and a demo project created using my create-baseline cli tool.
I then created a package.json
{
"name": "baseline-workspace",
"private": true,
"workspaces": [
"baseline",
"baseline-client",
"create-baseline",
"demo"
]
}Running npm install creates one shared node_modules folder and package-lock.json in this shared folder and all workspaces access this shared folder. If any project in the folder requests any other project that is part of the workspace, they receive the live, symlinked local folder.
Two modifications of the demo project are necessary. I had to change the version constraint to * so it is always satisfied with the local one instead of trying to install from my git source.
"dependencies": {
//"@codingkiwi/baseline": "git+https://git.codingkiwi.de/baseline/baseline.git",
"@codingkiwi/baseline": "*"
},
"devDependencies": {
//"@codingkiwi/baseline-client": "git+https://git.codingkiwi.de/baseline/baseline-client.git",
"@codingkiwi/baseline-client": "*",
},I also had to modify the compose.yaml so it mounts the parent folder which contains the shared workspace node_modules folder
working_dir: "/app/demo"
volumes:
# - ./:/app
- ../:/appIt comes with downsides though.
Projects in an npm workspace should not have their own package-lock.json. This is because they are resolved using the shared workspace lock file, which is one of the main reasons of using workspaces in the first place.
While this makes sense for monorepos, I still do not like monorepos as a concept. You can run npm install --no--workspaces inside each folder separately but you would have to do so by hand whenever you change a dependency or set up weird automations.
No package-lock.json means I cannot use npm ci which doubles npm install time in my test pipeline because it cannot use the hash based cache or skip package resolution. The "nice" side-effect is that it now tests if my library works against the newest available packages matching my semver constraints of the package.json in every run.
Quick side note:
I also tried out using file:/.. dependencies in the demo project instead of npm workspaces but this brought way more struggle since file dependencies are resolved as an isolated nested dependency tree. In practice this means the demo project cannot access drizzle-orm because the dependencies of the baseline package are not shared / mixed with the demo package, they live completely separate in /projects/baseline/node_modules.
In general this would be solved by defining these dependencies as peerDependencies but this requires adding all these peer dependencies into the dependencies of the demo project, shifting version and update management to the user, which is not what I want.
Another solution is to re-export the library, for example:
export * from "drizzle-orm"But this is not considered best practice.
Learning #5 - It is right to not have a lock file in a library. Transient dependencies (demo -> baseline -> drizzle-orm) are resolved using the semver constraints of baseline, not the lock file of baseline. Meaning if I require library at version ^1.0.0 and in the lock file I locked it at 1.0.1, a user running npm install baseline might probably get version 1.9.8 and not 1.0.1 since it just needs to satisfy the semver constraint ^1.0.0. It is my task as a library developer to ensure my constraints are right, just like it is the task of all other library developers to not introduce breaking changes in non-major versions.
Should you use it?
Probably not, there are more mature frameworks out there. Baseline is source-available, not open-source. I do not allow creating issues or merge requests in my repository since I simply do not have the time and AI has ruined open source.
I mostly created it for myself and for the experience of creating a framework itself, but if it sounds nice to you why not?



