youneed docs
← youneed
Menu ☰

Docs

One paradigm, ~170 packages in the monorepo. The sidebar groups them by ecosystem — dom, server, ssr, cli — each core package followed by adapters, providers, middleware and plugins that snap into it. Everything below is copied from the packages' own READMEs — nothing here is aspirational.

Introduction

youneed is a TypeScript-first toolkit for building web apps on native platform primitives — Custom Elements, Shadow DOM, the HTTP server, the Speculation Rules API — instead of a virtual DOM and a heavy runtime. Every package shares one paradigm: a factory returns a base class you extend, TC39 standard decorators register members into per-class registries, and a fluent builder wires it together. Component(tag), Controller(path), Page(url) and Test() all feel the same — learn the shape once on the client, reuse it on the server, in pages, in tests.

The dependency graph stays shallow on purpose: dom has zero dependencies, ssr builds on dom + server, devtools builds on dom + ssr. Install one package, or compose the stack — nothing drags the rest in.

Installation & quick start

Add the core client package and define a component.

$ pnpm add @youneed/dom
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>`;
  }
}
`; } }' aria-label="Copy code sample">Copy
A note on decorators. youneed uses TC39 standard decorators (@Component.define()), which no browser runs natively yet and which Vite's default oxc/esbuild target leaves untransformed. Running with tsx / node --import tsx handles them out of the box. Bundling with Vite needs @youneed/vite-plugin added so they're lowered.

dom @youneed/dom

A component framework built directly on native Custom Elements and Shadow DOM, with no virtual DOM. It combines Lit-style tagged-template rendering and scoped styles, Angular-style decorators, tasks and signals, and platform-native lifecycle — producing real custom elements that drop into plain HTML, React, Vue, or SSR markup without extra glue.

counter.ts @youneed/dom
import { Component, html, css } from "@youneed/dom";

@Component.define()
class Counter extends Component("x-counter") {
  static styles = css`button { font: inherit }`;

  @Component.prop() count = 0;          // reactive: assigning re-renders
  @Component.prop({ attribute: true }) label = "count"; // reflects <x-counter label="…">

  @Component.event() inc() { this.count++; } // auto-bound for @click

  render() {
    return html`<button @click=${this.inc}>${this.label}: ${this.count}</button>`;
  }
}
`; } }' aria-label="Copy code sample">Copy
packages/dom on GitHub →

dom-router @youneed/dom-router

A small client-side SPA router that mounts a Custom Element into a DOM outlet when the current URL matches a route. It supports three URL strategies — hash, history, query — behind a single API, and routes can target either a component class or a tag-name string. It pairs naturally with @youneed/dom components but has no hard dependency on it.

router.ts @youneed/dom-router
import { createRouter } from "@youneed/dom-router";

const router = createRouter({
  outlet: document.getElementById("app")!,
  mode: "history",                 // "hash" (default) · "history" · "query"
  routes: [
    { path: "/",            component: HomePage },       // a component CLASS…
    { path: "/users/:id",   component: "user-page" },    // …or a tag string. params: { id }
    { path: "*",            component: "not-found" },     // catch-all
  ],
});

router.navigate("/users/42");      // updates the URL + mounts <user-page>
router.current?.params;            // { id: "42" }
packages/dom-router on GitHub →

devtools @youneed/devtools

A floating, React-DevTools-style inspector for @youneed/dom applications. It provides a live, searchable component tree, a detail view with props, time-travel over state snapshots, a props diff, emitted events, live scheduler swapping, and per-element style editing — extensible with additional tabs (@youneed/ssr adds Page/Routes/Map views).

devtools.ts @youneed/devtools
import { installDevtools, mountDevtoolsPanel } from "@youneed/devtools";

installDevtools();        // capture per-component state/props/events/styles
mountDevtoolsPanel();     // floating, dockable, interactive panel (state persists)
packages/devtools on GitHub →

dom-adapter-react @youneed/dom-adapter-react

Bridge @youneed/dom and React in both directions. toReact renders a dom component inside a React tree — props are type-checked against the component's own @prop fields and on<Event> handlers receive its CustomEvents. fromReact wraps an existing React component as a custom element that drops into a dom tree — no rewrite. Part of the adapter family (dom-adapter-vue, -preact, -svelte, -astro, -angular).

bridge.tsx @youneed/dom-adapter-react
import { toReact, fromReact } from "@youneed/dom-adapter-react";

// dom → React: a real React component with typed props
const ReactUserCard = toReact(UserCard);
function Profile({ user }) {
  return <ReactUserCard user={user} onSelect={(e) => console.log(e.detail)} />;
}

// React → dom: a custom-element class wrapping a React component
const ReactChart = fromReact(Chart);
html`<${ReactChart.tagName} .props=${{ data }}></${ReactChart.tagName}>`;
packages/dom-adapter-react on GitHub →

dom-provider-i18n @youneed/dom-provider-i18n

Use @youneed/i18n translations inside components — call i18n("key") straight in an html template and re-render on every locale change. The i18nProvider form plugs into the component's providers slot and adds a typed this.i18n (key autocomplete) with automatic reactivity — no boilerplate, and it composes with other providers in the same array.

greeting.ts @youneed/dom-provider-i18n
import { Component, html } from "@youneed/dom";
import { createI18n } from "@youneed/i18n";
import { i18nProvider } from "@youneed/dom-provider-i18n";

const appI18n = createI18n({
  resources: { en: { hello: "Hello {name}" }, de: { hello: "Hallo {name}" } },
  locale: "en",
});

class Greeting extends Component("x-greeting", { providers: [i18nProvider(appI18n)] }) {
  render() {
    return html`<div>${this.i18n("hello", { name: "Ada" })}</div>`;
    //                       ^ typed: autocompletes "hello" and checks params
  }
}
// setLocale("de") → every subscribed component re-renders
packages/dom-provider-i18n on GitHub →

dom-provider-timers @youneed/dom-provider-timers

Lifecycle-scoped timers: this.timers wraps setTimeout / setInterval / rAF / idle / delay + the Scheduler API (postTask, yield) + debounce / throttle — everything is cancelled automatically when the component disconnects, and every handle (and the registry itself) implements Symbol.dispose, so using scopes it. The search box in this page's sidebar is debounced through it.

clock.ts @youneed/dom-provider-timers
import { Component, html } from "@youneed/dom";
import { timersProvider } from "@youneed/dom-provider-timers";

class Clock extends Component("x-clock", { providers: [timersProvider()] }) {
  time = this.signal(new Date());

  onMount() {
    this.timers.setInterval(() => this.time.set(new Date()), 1_000);
    // no teardown code — cancelled on disconnect
  }

  render() {
    return html`<time>${this.time.get().toLocaleTimeString()}</time>`;
  }
}

// delay rejects AbortError on disconnect; postTask maps to scheduler.postTask.
// Everything cancellable is Disposable:
{
  using tick = this.timers.setInterval(render, 100);
} // ← cancelled at end of scope, even on throw
packages/dom-provider-timers on GitHub →

server @youneed/server

A small, typed HTTP server built on node:http, offering decorator-based controllers, schema validation with type inference, guards, Express-style middleware, and content negotiation. It follows the same paradigm as the rest of the toolkit — extend a base class, mark methods with decorators, compose behavior with a fluent builder.

cats-controller.ts @youneed/server
import { Application, Controller, t, HttpError } from "@youneed/server";

const Cat = t.object({ name: t.string(), age: t.number() });

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

  @Controller.guard(isAdmin)            // per-method guard (stacks with class guards)
  @Controller.post({ body: Cat, response: { 201: Cat } })
  async create(ctx: Context) {
    return this.Response.json(ctx.body, { status: 201 });        // ctx.body is typed + validated
  }
}

Application(Cats)
  .use(requestLogger(), cors())          // global middleware (onion model)
  .openapi({ title: "Cats", version: "1.0.0" })   // → GET /openapi.json
  .listen(3000, (ctx) => console.log(`:${ctx.port}`));
packages/server on GitHub →

schema @youneed/schema

class-validator-style DTO validation built on standard TC39 decorators rather than reflect-metadata or legacy experimental decorators, so the same decorated class validates identically in TypeScript and plain compiled JS. Fields are annotated with constraint decorators like @IsEmail(), @MinLength(), and @Min(), and validate() checks a class or plain object against those rules.

schema.ts @youneed/schema
import { IsEmail, IsNotEmpty, MinLength, IsOptional, IsInt, Min, validate } from "@youneed/schema";

class CreateUserDTO {
  @IsEmail() email!: string;
  @IsNotEmpty() @MinLength(8) password!: string;
  @IsOptional() @IsInt() @Min(18) age?: number;
}

const errors = validate(CreateUserDTO, await req.json());
// [] when valid, else:
// [{ property: "email", value: "nope", constraints: { isEmail: "email must be an email" } }]
packages/schema on GitHub →

orm-sql @youneed/orm-sql

A small TypeORM-style SQL ORM built on standard TC39 decorators, with no reflect-metadata or legacy TypeScript decorator flags required. Entities are plain classes and databases plug in as adapters, with a zero-dependency SQLite adapter (node:sqlite) included as the reference engine — repositories give CRUD, relations, transactions, and schema migrations via a Migrator.

orm-sql.ts @youneed/orm-sql
import { Table, Orm, getRepository } from "@youneed/orm-sql";

class UsersTable extends Table("users") {
  @Table.primaryGeneratedColumn() id!: number;

  @Table.field("string")
  @Table.index({ group: "user_action" })
  userId!: string;

  @Table.field("string", { unique: true }) email!: string;
  @Table.column({ type: "boolean", default: true }) isActive!: boolean;

  @Table.oneToMany(() => Photo, (p) => p.user) photos!: Photo[];
}

await Orm({
  type: "sqlite",
  database: ":memory:",
  tables: [UsersTable, Photo],
  synchronize: true,
});

const users = getRepository(UsersTable);
const ada = await users.insert({ userId: "u1", email: "ada@x.com" });
await users.findOne({ email: "ada@x.com" }); // → UsersTable instance
packages/orm-sql on GitHub →

server-middleware-rate-limit @youneed/server-middleware-rate-limit

Rate-limit requests with a pluggable strategy — fixed window, sliding log, token bucket, exponential backoff — emitting standard X-RateLimit-* and Retry-After headers. The KV-backed KvFixedWindow keeps one shared limit across a whole fleet via a single atomic incr per request. One of ~30 server-middleware-* packages that compose the same way (cors, helmet, etag, session, metrics, …).

rate-limit.ts @youneed/server-middleware-rate-limit
import { Application } from "@youneed/server";
import { rateLimit, TokenBucket, KvFixedWindow } from "@youneed/server-middleware-rate-limit";
import { RedisKV } from "@youneed/kv-redis";

Application()
  .use(rateLimit({ windowMs: 60_000, max: 100 }))   // fixed window, global
  .use("/api", rateLimit({ strategy: new TokenBucket({ capacity: 50, refillPerSec: 5 }) }))
  // one shared limit across N instances — the counter lives in Redis
  .use("/auth", rateLimit({ strategy: new KvFixedWindow(new RedisKV({ url: process.env.REDIS_URL }), { windowMs: 60_000, max: 20 }) }))
  .listen(3000, () => {});
packages/server-middleware-rate-limit on GitHub →

server-plugin-jobs @youneed/server-plugin-jobs

A zero-dependency job scheduler — cron expressions (5- or 6-field), fixed intervals, one-off delays — shipped standalone (createScheduler) and as a server plugin that starts on listen and stops during graceful drain. A leader-lock over a shared KV store makes a job fire exactly once per occurrence across a fleet, and injectable clock/timers make tests run instantly.

jobs.ts @youneed/server-plugin-jobs
import { Application } from "@youneed/server";
import { jobs } from "@youneed/server-plugin-jobs";

const cron = jobs({
  jobs: [{ name: "cleanup", schedule: "0 */6 * * *", handler: purge }],
});

app.plugin(cron).listen(3000, () => {}); // start() on listen, stop() on drain

// still mutable after registration
cron.scheduler.add({ name: "heartbeat", schedule: { every: 30_000 }, handler: ping });
cron.scheduler.trigger("cleanup");       // run one now, bypassing the schedule
packages/server-plugin-jobs on GitHub →

ssr @youneed/ssr

Server-side rendering for @youneed/dom components, emitted as native Declarative Shadow DOM so markup hydrates without JavaScript, plus a Page entity that acts as the document-level counterpart to a Controller, with first-class Speculation Rules support. It requires registering a server DOM (happy-dom) before importing the package, since components extend HTMLElement at import time.

home.ts @youneed/ssr
import { Page, mountPages, enablePageDevtools } from "@youneed/ssr";
import { Application } from "@youneed/server";

class About extends Page("/about", { title: "About" }) {
  render() { return AboutApp; }                  // a component class, instance, or HTML string
}

class Home extends Page("/", {
  title: "Home",
  clientScript: () => import("./client.ts"),     // type-checked; resolved to a URL
  speculation: { prerender: [{ source: "list", urls: [About.url], eagerness: "moderate" }] },
}) {
  render() { return HomeApp; }
}

enablePageDevtools();                            // embed page+routes payload (dev)
mountPages(Application(), Home, About).listen(3010,);
packages/ssr on GitHub →

ssr-plugin-meta @youneed/ssr-plugin-meta

SEO <meta> + OpenGraph + Twitter Card tags as SSR page middleware. A page declares metadata via the meta option; the module renders the tags, resolving og:url / og:image to absolute URLs against the SSR origin. meta can also be a function of the request context for per-request tags.

post.ts @youneed/ssr-plugin-meta
import { ssr } from "@youneed/server-plugin-ssr";
import { meta } from "@youneed/ssr-plugin-meta";

class Post extends Page("/blog/:slug", {
  title: "Hello world",
  meta: {
    description: "An introductory post.",
    og: { type: "article", image: "/og/hello.png" },
    twitter: { card: "summary_large_image" },
  },
}) { /* … */ }

app.plugin(ssr({
  origin: "https://example.com",
  pages: [Post],
  modules: [meta({ siteName: "Example", twitterSite: "@example" })],
}));
packages/ssr-plugin-meta on GitHub →

ssr-plugin-sitemap @youneed/ssr-plugin-sitemap

A sitemap.xml module for the SSR plugin. Static page routes are enumerated automatically; dynamic routes (/users/:id) are listed via entries — a value or an async function, so the feed reflects fresh data on each request. All <loc>s resolve absolute against origin.

sitemap.ts @youneed/ssr-plugin-sitemap
import { ssr } from "@youneed/server-plugin-ssr";
import { sitemap } from "@youneed/ssr-plugin-sitemap";

app.plugin(ssr({
  origin: "https://example.com",
  pages: [Home, About, Pricing],
  modules: [
    sitemap({
      exclude: ["/admin", /^\/internal/],
      entries: [{ url: "/blog/launch", lastmod: "2026-06-01", priority: 0.8 }],
      defaults: { changefreq: "weekly", priority: 0.5 },
    }),
  ],
}));
packages/ssr-plugin-sitemap on GitHub →

ssr-plugin-robots @youneed/ssr-plugin-robots

A robots.txt module: per-user-agent allow/disallow policies, a Sitemap: line resolved against origin, and the permissive "allow everything" file when no policies are given.

robots.ts @youneed/ssr-plugin-robots
import { ssr } from "@youneed/server-plugin-ssr";
import { robots } from "@youneed/ssr-plugin-robots";

app.plugin(ssr({
  origin: "https://example.com",
  modules: [
    robots({
      policies: [
        { userAgent: "*", disallow: ["/admin", "/api"], allow: "/api/public" },
        { userAgent: ["GPTBot", "CCBot"], disallow: "/" },
      ],
      sitemap: true, // → Sitemap: https://example.com/sitemap.xml
    }),
  ],
}));
packages/ssr-plugin-robots on GitHub →

cli @youneed/cli

A type-safe, Commander-style CLI framework built on the same factory-class pattern as @youneed/dom's Component and @youneed/server's Controller. Options and commands are defined as classes and composed into an Application, with this.options and execute(...) typed directly from flag/argument strings — plus a small reactive layer, graceful shutdown, middleware, and plugins.

cli.ts @youneed/cli
import { Application, Command, Option, defaultOptions } from "@youneed/cli";

// A reusable, named option (its key + value type flow into `this.options`).
class FirstOption extends Option("--first", {
  short: "f",
  description: "display just the first substring",
}) {}

class SplitCommand extends Command({
  name: "split <string>", // grammar: a word + positional args
  description: "Split a string into substrings and display as an array",
  options: [FirstOption, { name: "-s, --separator <char>", default: "," }, ...defaultOptions()],
}) {
  execute(value: string) {
    // `value` is typed from `<string>`; `this.options` from the options tuple.
    const limit = this.options.first ? 1 : undefined;
    console.log(value.split(this.options.separator, limit));
  }
}

Application({
  name: "string-util",
  description: "CLI to some JavaScript string utilities",
  version: "0.0.8",
  commands: [SplitCommand],
  options: [...defaultOptions()],
});
packages/cli on GitHub →

cli-middleware-prompt @youneed/cli-middleware-prompt

Interactive prompts for CLI commands: the middleware adds this.prompt with ask (free text), confirm (y/n), choice (single-select), list (multi-select), alert and spinner. Prompts draw through the core LiveRenderer in raw-key mode, and everything binds to one terminal — inject a scripted double and they're testable.

setup.ts @youneed/cli-middleware-prompt
import { Application, Command } from "@youneed/cli";
import { prompts } from "@youneed/cli-middleware-prompt";

class Setup extends Command("setup", { middleware: [prompts()] }) {
  async execute() {
    const name = await this.prompt.ask("Project name?", { default: "app" });
    const env = await this.prompt.choice("Environment", ["dev", "staging", "prod"]);
    const feats = await this.prompt.list("Features", ["ts", "lint", "tests"]);
    if (await this.prompt.confirm(`Create ${name}?`, { default: true })) {
      await this.prompt.spinner("scaffolding", () => scaffold(name, env, feats));
      await this.prompt.alert("Done!");
    }
  }
}

Application({ name: "create", commands: [Setup] }).run(["setup"]);
packages/cli-middleware-prompt on GitHub →

cli-plugin-help @youneed/cli-plugin-help

Enhanced help: registers a help [command] command that replaces the built-in output with a grouped command list and per-command examples — the interactive, in-terminal usage screen. For offline man(1) documentation, pair it with cli-plugin-man.

ops.ts @youneed/cli-plugin-help
import { Application } from "@youneed/cli";
import { help } from "@youneed/cli-plugin-help";

Application({
  name: "ops",
  version: "1.0.0",
  description: "Operations toolkit",
  commands: [/* … */],
  plugins: [
    help({ examples: { split: ["ops split a,b,c --first"] } }),
  ],
}).run();

// ops help          → grouped command list with examples
// ops help split    → usage, options and examples for one command
packages/cli-plugin-help on GitHub →

test @youneed/test

A class-and-decorator test framework built in the same paradigm as @youneed/dom and @youneed/server: a factory returns a base class you extend, TC39 decorators register test members (cases, fixtures, hooks), and a fluent TestApplication builder wires everything up and runs it. Fixtures give scoped setup/teardown, TestContext carries steps/annotations/abort signals, and reporters, plugins, and parallelization (worker processes, CI sharding) are pluggable.

calculator.test.ts @youneed/test
import { Test, Fixture, TestApplication, expect } from "@youneed/test";

class Calculator {
  add(a: number, b: number) { return a + b; }
}

class CalcFixture extends Fixture<Calculator>({ name: "calc", scope: "test" }) {
  setup() { return new Calculator(); }
}

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);
  }
}

await TestApplication().addTests(CalcTest).run();
packages/test on GitHub →

logger @youneed/logger

A zero-dependency, Winston-style structured logger with pluggable transports and a composable format pipeline. The core touches no Node-only APIs, so the same bundle runs in the browser, in SSR/SSG, on the server, in workers, and at the edge — the only built-in destination is a universal ConsoleTransport, while environment-specific destinations ship as separate @youneed/logger-transport-<name> packages. Child loggers, secret redaction, and TC39 resource management for disposal round it out.

logger.ts @youneed/logger
import { createLogger, format, ConsoleTransport } from "@youneed/logger";

const log = createLogger({
  level: "info",
  format: format.combine(format.timestamp(), format.redact(["ssn"]), format.json()),
  defaultMeta: { service: "api" },
  transports: [new ConsoleTransport()], // works in the browser and on the server
});

log.info("listening", { port: 3000 });
// {"level":"info","message":"listening","timestamp":"…","service":"api","port":3000}

const reqLog = log.child({ requestId: "r-42" }); // bindings on every record
reqLog.error("db down", { password: "hunter2" }); // password → "[REDACTED]"
packages/logger on GitHub →

Naming & the full index

Around 170 packages live in the monorepo. Most are an extension of a core package, named <core>-<kind>-<name>:

dom-provider-* A this.*-style extension for Component — e.g. dom-provider-a11y, dom-provider-i18n, dom-provider-rbac.
dom-adapter-* Interop with another view layer — dom-adapter-react, dom-adapter-vue, dom-adapter-svelte, dom-adapter-astro.
server-plugin-* A ServerPlugin bolt-on for @youneed/serverserver-plugin-graphql, server-plugin-oauth2, server-plugin-jobs.
server-middleware-* Onion-model middleware for the core server — server-middleware-logger, server-middleware-idempotency.
cli-middleware-* Middleware for @youneed/cli commands — cli-middleware-prompt, cli-middleware-progress, cli-middleware-color.
logger-transport-* A destination for @youneed/loggerlogger-transport-stdout, -file, -http.
orm-adapter-* A dialect/driver for the ORMs — orm-adapter-mysql, orm-adapter-postgres, orm-adapter-mongo.
test-reporter-* / test-plugin-* Output formats and plugins for @youneed/test — reporters render results, plugins add behavior (benchmark, snapshot, resilience).

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

The full index

The same <yn-package-explorer> component as on the landing page — descriptions come straight from each package's package.json.

packages/ on GitHub →