A type-safe CSS-in-TypeScript library and no-magic HTTP router for Deno.
@tub/web merges two capabilities into one package:
- CSS: scoped class generation, CSS variable theming, at-rules, and style composition — all at build time with zero runtime overhead.
- Web server: a standards-based HTTP router built directly on
Deno.ServeHandler, a directory-based route builder, and asite()convenience function for full-stack Deno applications.
deno add jsr:@tub/webimport { render, style } from "@tub/web";
const button = style({
backgroundColor: "blue",
color: "white",
padding: "8px 16px",
borderRadius: "4px",
});
// Use in DOM
element.className = button.toString();
// Render CSS
const css = render(button);import { render, style } from "@tub/web";
const card = style({
padding: "16px",
transition: "transform 0.2s",
select: {
"&:hover": { transform: "scale(1.02)" },
"& > h2": { marginTop: "0" },
"@media (min-width: 768px)": { padding: "24px" },
},
});
console.log(render(card));import { join, render, style } from "@tub/web";
const body = style("body", { margin: "0", fontFamily: "sans-serif" });
const header = style("#header", { position: "fixed", top: "0" });
console.log(render(join(body, header)));import { fallback, join, render, style, theme } from "@tub/web";
const colors = theme({
colors: {
primary: "blue",
secondary: "green",
brand: { light: "#eef", dark: "#335" },
},
spacing: "8px",
});
const card = style({
color: colors.colors.primary,
backgroundColor: colors.colors.brand.light,
padding: colors.spacing,
});
const button = style({
color: colors.colors.primary,
borderColor: fallback(colors.colors.secondary, "gray"),
});
const lightTheme = style(":root", colors);
const darkColors = colors.create({
colors: {
primary: "white",
secondary: "#ccc",
brand: { light: "#335", dark: "#eef" },
},
});
const darkTheme = style(".dark", darkColors);
console.log(render(join(lightTheme, darkTheme, card)));import { render, style, use } from "@tub/web";
const base = style({ padding: "8px" });
const primary = style({ backgroundColor: "blue", color: "white" });
const large = style({ fontSize: "1.25rem" });
const className = use(base, primary, large);
// "abc123 def456 ghi789"import { at, join, render } from "@tub/web";
const roboto = at("@font-face", {
fontFamily: "Roboto",
src: "url('/fonts/roboto.woff2') format('woff2')",
fontWeight: "400",
fontDisplay: "swap",
});
const themeColor = at("@property --theme-color", {
syntax: '"<color>"',
inherits: "true",
initialValue: "blue",
});
const thumbs = at("@counter-style thumbs", {
system: "cyclic",
symbols: "👍",
suffix: " ",
});
console.log(render(join(roboto, themeColor, thumbs)));import {
MINIMAL_RENDER_OPTIONS,
render,
STANDARD_RENDER_OPTIONS,
style,
} from "@tub/web";
const button = style({ color: "blue" });
console.log(render(button)); // human-readable
console.log(render(button, STANDARD_RENDER_OPTIONS)); // same as default
console.log(render(button, MINIMAL_RENDER_OPTIONS)); // minifiedimport { properties, style } from "@tub/web";
const flexCenter = properties({
display: "flex",
alignItems: "center",
justifyContent: "center",
});
const card = style({ ...flexCenter, padding: "16px" });import { group, render, style } from "@tub/web";
const g = group({
button: {
primary: style({ backgroundColor: "blue", color: "white" }),
secondary: style({ backgroundColor: "gray", color: "black" }),
},
text: {
heading: style({ fontSize: "2rem", fontWeight: "bold" }),
body: style({ fontSize: "1rem" }),
},
});
element.className = g.button.primary.toString();
console.log(render(g));Deno provides a powerful, standards-compliant HTTP server. Most router modules
still implement patterns from Node.js — untyped middleware, magic next
callbacks, and automagical conventions. @tub/web takes a different approach:
- No templating tools or magic project setup.
- Builds directly on
Deno.ServeHandlerand web standards. - Type-safe path parameter parsing at the type level.
- Composes well enough to implement both SSR and autodocumenting API frameworks.
import { context, handle, html, router } from "@tub/web/router";
const app = router(context({}), {
routes: [
handle("GET /", () => html("<h1>Welcome!</h1>")),
handle(
"GET /users/:id",
(_, params) => html(`<h1>User: ${params.id}</h1>`),
),
],
});
Deno.serve(app.handle);The route builder walks a directory and converts exported tokens into routes. Place route files anywhere under the root path — the file's path relative to root becomes the URL pattern.
// routes/api/users.ts
import { get, post } from "@tub/web/tokens";
import * as R from "@tub/web/router";
// GET /api/users
export const list = get((_req, _params, _ctx) =>
R.json(JSON.stringify([{ id: 1, name: "Alice" }]))
);
// POST /api/users
export const create = post(async (req, _params, _ctx) => {
const body = await req.json();
return R.json(JSON.stringify(body), R.STATUS_CODE.Created);
});// routes/users/:id.ts
import { del, get } from "@tub/web/tokens";
import * as R from "@tub/web/router";
// GET /users/:id — params.id is type-checked
export const show = get((_req, params, _ctx) =>
R.json(JSON.stringify({ id: params.id }))
);
// DELETE /users/:id
export const remove = del((_req, _params, _ctx) =>
R.text("Deleted", R.STATUS_CODE.NoContent)
);site() is the fastest way to get a full-stack Deno app running. It wires
together the client SPA plugin, server routes plugin, and static file plugin,
then returns a request handler ready for Deno.serve.
import { site } from "@tub/web";
Deno.serve(
await site({
root_path: "./src/routes",
site_name: "My Application",
unsafe_import: (path) => import(path),
}),
);Use the individual plugins when you need finer control:
import { build } from "@tub/web/builder";
import { server_plugin } from "@tub/web/plugin_server";
import { static_plugin } from "@tub/web/plugin_static";
import { openapi_plugin } from "@tub/web/plugin_openapi";
import { deno_fs } from "@tub/web/deno_fs";
import { context, router } from "@tub/web/router";
const result = await builder({
root_path: "./src/routes",
fs: deno_fs,
unsafe_import: (path) => import(path),
plugins: [
server_plugin({ middleware: [authMiddleware] }),
static_plugin({ exclude_extensions: [".ts", ".tsx"] }),
openapi_plugin({ info: { title: "My API", version: "1.0.0" } }),
],
}).build();
const app = router(context({}), {
routes: result.site_routes.map((r) => r.route),
});
Deno.serve(app.handle);Route handlers can declare typed params and body schemas. The server plugin validates incoming requests automatically and returns 400 on failure.
import { get, post } from "@tub/web/tokens";
import * as R from "@tub/web/router";
export const get_user = get({
params: (s) => s.struct({ id: s.string() }),
handler: (_req, params, _body, _ctx) => R.json(JSON.stringify(params)),
});
export const create_user = post({
body: (s) => s.struct({ name: s.string(), age: s.number() }),
handler: async (_req, _params, body, _ctx) =>
R.json(JSON.stringify(body), R.STATUS_CODE.Created),
});import { middleware } from "@tub/web/router";
const log = middleware((next) => async (req, params, ctx) => {
ctx.logger.info(`${req.method} ${req.url}`);
return next(req, params, ctx);
});style(input)— Creates a Style with an auto-generated class namestyle(selector, input)— Creates a Style with a custom selectorrender(style, options?)— Renders a HasStyles object to a CSS stringjoin(...styles)— Combines multiple HasStyles objects into one for renderinguse(...styles)— Combines multiple styles into a class name stringproperties(input)— Identity function for type-checked style objectsgroup(shape)— Creates a StyleGroup from a tree of styleshasStyles(value)— Type guard for HasStyles objects
theme(values)— Creates a type-safe CSS variable themeTheme.create(partial)— Creates a variant with merged valuesfallback(ref, ...values)— Adds fallback values to a CSS variable referenceisTheme(value)— Type guard for Theme objects
at(rule, properties)— Creates styles for unnestable at-rules
STANDARD_RENDER_OPTIONS— Pretty-printed CSS with newlines and indentationMINIMAL_RENDER_OPTIONS— Minified CSS with no whitespace
router(ctx, config)— Creates a router instancecontext(state, logger?)— Creates a context object for route handlershandle(routeString, handler)— Type-safe route definition from a stringroute(method, path, handler)— Route definition from partsmiddleware(m)— Utility for creating typed middlewarehtml(content, status?)— HTML response helperjson(content, status?)— JSON response helpertext(content, status?)— Plain text response helperresponse(body, init?)— Generic response helperresponse_init(status, headers?, statusText?)— Creates a ResponseInitSTATUS_CODE— HTTP status code constantsSTATUS_TEXT— HTTP status text constantsHEADER— HTTP header name constantsMETHODS— HTTP method constants
Method builders for route files: get, post, put, del, patch, head,
options. Each accepts a plain handler or a config object with params,
body, output, and handler.
Client page factories for SPA builds: client_route, client_default,
client_index, client_wrapper.
server_plugin(opts?)— Scans files for exported tokens and builds routesstatic_plugin(opts?)— Serves files directly from the filesystemclient_plugin(opts?)— Bundles Preact components into a client SPAopenapi_plugin(opts)— Generates an OpenAPI 3.1.0 spec from all routes
builder(config)— Creates a builder that walks a directory and runs plugins
deno_fs— Deno filesystem implementation for the route builder
- vanilla-extract — Primary inspiration for
the CSS API design, particularly
style, theming patterns - Sass — Influence on nesting and selector composition
- fp-ts — Functional programming patterns
- Oxide Computer Company's Dropshot — Inspiration for autodocumenting API design
Contributions are welcome! This is an experimental project.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
# Run tests
deno test
# Type check
deno check mod.ts
# Format
deno fmtMIT License - see LICENSE for details.