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";constbutton=style({backgroundColor: "blue",color: "white",padding: "8px 16px",borderRadius: "4px",});// Use in DOMelement.className=button.toString();// Render CSSconstcss=render(button);import{render,style}from"@tub/web";constcard=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";constbody=style("body",{margin: "0",fontFamily: "sans-serif"});constheader=style("#header",{position: "fixed",top: "0"});console.log(render(join(body,header)));import{fallback,join,render,style,theme}from"@tub/web";constcolors=theme({colors: {primary: "blue",secondary: "green",brand: {light: "#eef",dark: "#335"},},spacing: "8px",});constcard=style({color: colors.colors.primary,backgroundColor: colors.colors.brand.light,padding: colors.spacing,});constbutton=style({color: colors.colors.primary,borderColor: fallback(colors.colors.secondary,"gray"),});constlightTheme=style(":root",colors);constdarkColors=colors.create({colors: {primary: "white",secondary: "#ccc",brand: {light: "#335",dark: "#eef"},},});constdarkTheme=style(".dark",darkColors);console.log(render(join(lightTheme,darkTheme,card)));import{render,style,use}from"@tub/web";constbase=style({padding: "8px"});constprimary=style({backgroundColor: "blue",color: "white"});constlarge=style({fontSize: "1.25rem"});constclassName=use(base,primary,large);// "abc123 def456 ghi789"import{at,join,render}from"@tub/web";constroboto=at("@font-face",{fontFamily: "Roboto",src: "url('/fonts/roboto.woff2') format('woff2')",fontWeight: "400",fontDisplay: "swap",});constthemeColor=at("@property --theme-color",{syntax: '"<color>"',inherits: "true",initialValue: "blue",});constthumbs=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";constbutton=style({color: "blue"});console.log(render(button));// human-readableconsole.log(render(button,STANDARD_RENDER_OPTIONS));// same as defaultconsole.log(render(button,MINIMAL_RENDER_OPTIONS));// minifiedimport{properties,style}from"@tub/web";constflexCenter=properties({display: "flex",alignItems: "center",justifyContent: "center",});constcard=style({ ...flexCenter,padding: "16px"});import{group,render,style}from"@tub/web";constg=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";constapp=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.tsimport{get,post}from"@tub/web/tokens";import*asRfrom"@tub/web/router";// GET /api/usersexportconstlist=get((_req,_params,_ctx)=>R.json(JSON.stringify([{id: 1,name: "Alice"}])));// POST /api/usersexportconstcreate=post(async(req,_params,_ctx)=>{constbody=awaitreq.json();returnR.json(JSON.stringify(body),R.STATUS_CODE.Created);});// routes/users/:id.tsimport{del,get}from"@tub/web/tokens";import*asRfrom"@tub/web/router";// GET /users/:id — params.id is type-checkedexportconstshow=get((_req,params,_ctx)=>R.json(JSON.stringify({id: params.id})));// DELETE /users/:idexportconstremove=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(awaitsite({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";constresult=awaitbuilder({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();constapp=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*asRfrom"@tub/web/router";exportconstget_user=get({params: (s)=>s.struct({id: s.string()}),handler: (_req,params,_body,_ctx)=>R.json(JSON.stringify(params)),});exportconstcreate_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";constlog=middleware((next)=>async(req,params,ctx)=>{ctx.logger.info(`${req.method}${req.url}`);returnnext(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.