DEV Community

Slee Woo
Slee Woo

Posted on

How many Improvements needed for an Innovation?

A speculative question, perhaps. But it reveals a bare truth: improvements don't make innovations.

Improvements are pieces of a puzzle. Refined and sharpened with every new version, they are absolutely necessary to form the puzzle.
But no matter how polished they are, they remain pieces of a flat puzzle.

Innovation is when multiple puzzles compose into a new structure.

Back in 2009, Google was polishing the pieces of their remarkable V8 engine.
Then Ryan Dahl proved once more that everything ingenious is simple - compose the V8 puzzle with other state-of-the-art puzzles, and a new structure emerges.

That's an innovation.


Today we have plenty of brilliantly polished, state-of-the-art frameworks.
Each performs perfectly well on its own. Each with lot of features and few of limitations - each a complete puzzle in its own right.

And that's entirely normal. Developers choose a preferred stack and excel in that direction.

The ecosystem thrives on specialization - Vue devs feeling at home in Vue, React developers building confidently in React, Hono enthusiasts enjoying every routing decision.

Everyone productive and happy within their chosen stack.
Many shipping full projects as a monolith - one stack, one structure, no complaints.

Still, there is a limitation. Not in the frameworks themselves - they are perfect (though not ideal).

The limitation is the lack of composition.

The typical workflow is to choose a framework and plumb everything into a monolithic project.
Simple enough: if I choose framework X, every part of my app uses X.
Regardless of concern, every part is coupled to the same stack.

From one side, that's ideal. Same API across the whole project - no shifts, no drifts.

But from the other side - admin is one concern, marketing site is another,
customer app is yet another, and none of them has much to do with the others.
Each under its own base path, each working just fine in isolation.

Perhaps that's just fine. But this is horizontal scaling - ideal for distributed systems,
barely fitting the development realm.

There has to be a better way to think about this.

What if separate concerns became separate apps?

Each with its own framework, base URL, build pipeline, and deployment strategy.

As a result, a compound meta-app comprising multiple purpose-specific apps, each on its own stack:

  • admin on Vue, using Koa for the backend
  • marketing SSR site on Svelte, no backend needed
  • customer app on React, using Express for the backend
  • frontend on SolidJS, using Hono for the backend

And the composition isn't done for the sake of composition alone.

The new structure acts as a universal chassis - providing the same consistent way to define routes for all apps, regardless of framework, backend or frontend.

That's the unified routing pattern.

And a way to define validation rules directly in TypeScript, without using yet another validation lib.

That's the unified validation pattern.

Also a unified development workflow and a unified build pipeline.

And each app deployable independently: admin on company servers,
marketing on Cloudflare edge, customer app on Vercel.

Yet, all under the same roof - same routing patterns, same validation logic, same node_modules.

Now, the project is no longer flat. It becomes multi-dimensional - multiple parts composed into a new structure.

One structure delivering multiple services, each representing a specific concern.

No excessive proprietary abstractions. Just a unified routing pattern and the full freedom to use the preferred framework for every concern.

One infrastructure that spans multiple stacks.


Wait, what is this all about? Smells like marketing... Show me the code!

Ok, let's proceed with a conceptual spec - one that happens to be pretty close to a real implementation.

The unified routing pattern

As with any routing system, a route consists of multiple segments.
A route segment takes static parts and dynamic params.
Params can be required, optional, or splat.

  • Required params use square brackets: [paramName]
  • Optional params use curly braces: {paramName}
  • Splats use the spread syntax: {...paramName}

As a result, a simple routing pattern to define routes across all frameworks,
rather than juggling multiple syntaxes that differ from framework to framework.

And if implementing file-based routing, the chassis can scan route hierarchy and wire each route into the under-the-hood framework router automatically.

Create a route file using the unified pattern - get a route mounted into the framework. No framework-specific syntax to learn. No manual wiring.

The unified validation pattern

After long years of using various validation libs, I arrived at a simple truth:
we already use the most accurate and sharp validation instrument in the JavaScript world.

That's the TypeScript - our trusted compile-time safety net for years.

Why not use it for runtime validation too? That's the most natural path, the path of least resistance.

The implementation could be pretty simple - use TypeScript's AST parser to get a plain representation of a type, resolve references, and convert it into JSON Schema - the de-facto standard in validation.

Then use a reliable, high-performance lib like TypeBox to validate at runtime.

No more convoluted validation syntax to learn and maintain.
No more dissonance switching between a project using Zod and one using ArkType.
No more schema duplication.
Single source of truth: your TypeScript types.

How params validation might look like:

defineRoute<"users/[id]", [
  number // validate id as number
]>(({ GET }) => [
  GET(async (ctx) => {
    // id is validated as a number at runtime
  }),
]);
Enter fullscreen mode Exit fullscreen mode

The same pattern applies naturally to payload validation - validating JSON or form data on POST requests:

defineRoute<"posts">(({ POST }) => [
  POST<{
    json: {
      title: string;
      content: string;
      tags: string[];
    },
  }>(async (ctx) => {
    // JSON is validated and properly typed before reaching here
  }),
]);
Enter fullscreen mode Exit fullscreen mode

Pretty solid so far, but that's half the story - knowing a value is a string tells you very little. What about empty strings? Strings that are too long?

Turns out, half the problem is already solved - we're converting types to JSON Schema, and JSON Schema has constraints for every use case.

The only question is how to pass constraints from a type into the schema.

A generic wrapper type, perhaps?

Actually yes! TypeBox v1 has this built-in already.

Simply define a passthrough Options generic and reuse it across all your types:

// declare globally or import from somewhere
type Options<T, _JsonConstraints> = T;

POST<{
  json: {
    name: Options<string, { minLength: 1, maxLength: 255 }>;
    email: Options<string, { format: "email" }>;
    postalCode: Options<string, { pattern: "^[0-9]{5}(-[0-9]{4})?$" }>;
  },
}>
Enter fullscreen mode Exit fullscreen mode

TypeBox uses the first type argument as the validated type and the second as constraints - naturally building a JSON Schema with proper rules.

And why stop at request validation? Why not validate responses too?

import type { User } from "./types";

defineRoute<"users">(({ GET }) => [
  GET<{
    response: [200, "json", User],
  }>(async (ctx) => {
    // response must comply to the defined schema
  }),
]);
Enter fullscreen mode Exit fullscreen mode

Simple, Native, Universal - works across all frameworks.
Whatever you choose - Hono, Koa, Fastify, Express - the validation pattern is identical: your TypeScript types.

More niceties that scale just as well

Given that the chassis masters routes for all frameworks universally,
it can apply further useful patterns across the board.

Nested Layouts for frontends - detect a special file, say layout.tsx,
and wrap all underlying routes into the exported layout component.
The chassis wires it into the under-the-hood framework automatically.
You define a layout file. The hard work is on the chassis.

Cascading Middleware for backends - detect a special file, say use.ts,
and automatically wire the exported middleware into all underlying routes.
No manual importing, no repetition. One file, entire subtree covered.

And once the chassis already has an AST for every route -
knowing its params, HTTP methods, accepted payloads, and response shapes -
why not also generate typed Fetch Clients?

Even better - generated Fetch Clients can use the same high-performance validation routines as the server. Only validated requests reach the server, saving bandwidth and giving users instant feedback.

And while we're at it - why not generate a full OpenAPI spec automatically?

That's just a few of the possibilities a universal chassis pattern opens up. The only limit is imagination.


So, is this all speculation? Or is there a real project behind it?

Well, this is a reproduction of how I've spent my spare time for about two years :)

And yes, there is a real project behind this concept - meet KosmoJS, the composable meta-framework.

How could it take so long to build something in the era of AI agents, you ask?

Fair question. With a fair answer: this is not yet another vibe-coded thing.

LLMs helped a lot, but every decision was thought and rethought multiple times.
They provided inspiration and assisted with various implementation challenges. But no more than that.

And to be honest, it wasn't the implementation that took so long.

It was the spec - which changed multiple times, until the most natural and least resistant way to express the simplicity was found.

That's it, for a spec to resist in time, it must be instantly understandable, effortlessly adoptable, and perfectly memorable.
No LLM in the world will suggest the ideal spec that clicks with every user.

That said, KosmoJS was built from the needs and annoyances accumulated over 10 years of JavaScript full-stack development (preceded by another 10 years of backend work - first PHP, then Ruby (no Rails) - but that's another story :)

It may not, and should not, meet everyone's expectations or needs.
It's not aimed at competing with existing solutions.
It simply brings another approach to organizing full-stack applications.

Here is a Playground

Suggestions and fair criticism are kindly welcome!

Top comments (0)