Native primitives, one paradigm.

youneed is a TypeScript-first toolkit for building web apps on what the platform already ships — Custom Elements, Shadow DOM, the HTTP server, the Speculation Rules API. No virtual DOM. Minimal runtime.

counter.ts @youneed/dom
@Component.define()
class Counter extends Component("x-counter") {
  @Component.prop() count = 0;
  @Component.event() inc() { this.count++; }
  render() {
    return html`<button @click=${this.inc}>${this.count}</button>`;
  }
}
~170packages in the monorepo
0dependencies in @youneed/dom
50kreq/s on the repo’s local bench (autocannon)
MITlicense

The same shape, at every layer.

A factory returns a base class you extend. TC39 standard decorators register the members. A fluent builder wires it together. Component(tag), Controller(path), Page(url) and Test() all feel the same — learn it once on the client, reuse it on the server, in pages, in tests.

Type the HTTP layer.

A controller is a class on a path. Route decorators carry the schema, so ctx.params and ctx.body arrive typed and validated — guards and middleware compose around them.

On this repo’s local autocannon bench the server holds ~50k req/s — one machine, no hosted asterisk.

cats.ts @youneed/server
const Cat = t.object({ name: t.string(), age: t.number() });

class Cats extends Controller("/cats") {
  @Controller.get("/:name", { response: { 200: Cat } })
  async byName(ctx: Context) {
    const cat = lookup(ctx.params.name);
    if (!cat) throw new HttpError(404, { error: "Not found" });
    return cat; // validated against 200: Cat
  }
}

Application(Cats).listen(3000);

Render pages on the server.

A page is a class on a URL. It renders Declarative Shadow DOM on the server, names its own client script, and declares Speculation Rules so the browser prerenders the next navigation.

home.ts @youneed/ssr
class Home extends Page("/", {
  title: "Home",
  clientScript: () => import("./client.ts"),
  speculation: { prerender: [{ source: "list", urls: [About.url] }] },
}) {
  render() { return HomeApp; }
}

mountPages(Application(), Home, About).listen(3010);

Test in the same shape.

Suites extend Test(), cases are decorated methods, fixtures inject through a field. If you can write a component, you already know how to write the test.

calc.test.ts @youneed/test
class CalcTest extends Test({ name: "Calculator" }) {
  @Test.use(CalcFixture) calc!: Calculator;

  @Test.it("adds two numbers")
  adds() {
    expect(this.calc.add(2, 3)).toBe(5);
  }
}

Pick only what you need.

Around 170 packages live in the monorepo. The dependency graph stays shallow: dom has zero dependencies, ssr builds on dom + server, devtools on dom + ssr. The key ones:

PackageWhat it doesBuilds on
@youneed/domReactive components on Custom Elements + Shadow DOMnothing — zero dependencies
@youneed/serverTiny typed HTTP server — controllers, schema validation, guards, middlewarenode:http
@youneed/ssrDeclarative Shadow DOM SSR + Page entity with Speculation Rulesdom + server
@youneed/dom-routerSPA router — hash, history, querydom
@youneed/devtoolsFloating inspector — component tree, time-traveldom + ssr
@youneed/testClass + decorator test framework
@youneed/orm-sqlTypeORM-style ORM on TC39 decorators, built-in SQLitenode:sqlite
@youneed/cliCommander-style CLI framework
@youneed/schemaclass-validator-style DTO validation
@youneed/loggerStructured JSON logging

Every package follows the same convention: a factory, a base class, standard decorators. Install one, or compose the stack — nothing drags the rest in.

The full catalog

Every package in the monorepo — search by name or description, or narrow to one ecosystem. Descriptions come straight from each package's package.json. (This table is itself a @youneed/dom component.)

Start with one package.

Add @youneed/dom, define a tag, ship a component. The server, SSR, router and tests join the same paradigm when you need them.

$ pnpm add @youneed/dom