webspresso

Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling.

You describe routes as files under pages/, render with Nunjucks, validate HTTP input with Zod, and optionally plug in Knex-backed models, migrations, and first-party plugins (dashboard, sitemap, analytics, admin, and more). This page is a condensed reference; the authoritative source is the README.md in the repository.

Stack: Express · Nunjucks · Knex · Zod · Node 18+

At a glance

Webspresso is an opinionated, batteries-included server-side toolkit: one CLI scaffolds projects, watches your pages/ tree to build Express routes, and optional plugins add SEO, analytics, and an admin UI without bolting on a separate meta-framework. It targets teams who want Laravel- or Rails-like ergonomics on Node with transparent files on disk instead of a hidden compiler graph.

When it fits

  • Content-heavy sites and internal tools with SSR
  • JSON APIs colocated with HTML routes
  • PostgreSQL / MySQL / SQLite with a small ORM

Core ideas

  • Convention over configuration for URLs
  • Validation and types via Zod at the edge
  • Plugins fail soft (warn) so one bad plugin does not stop the app

Features

Installation

Requires Node.js 18+. Install the CLI globally or add the framework as a project dependency.

npm install -g webspresso
# or, inside a project:
npm install webspresso

After npm install webspresso, use npx webspresso or define an npm script if the binary is not on your PATH.

Peer dependencies

Database drivers, Faker, and dotenv are optional peers — install only what you use so production images stay lean.

PackageTypical use
pgPostgreSQL via Knex
mysql2MySQL / MariaDB
better-sqlite3SQLite (local dev, embedded)
dotenvLoad .env in development
@faker-js/fakerSeed scripts and factories

Quick start

New projects ship with Tailwind CSS, a starter layout, i18n (e.g. en/tr), and npm scripts. Pass --no-tailwind to skip Tailwind.

webspresso new my-app
cd my-app
npm install
npm run build:css
webspresso dev
# or: npm run dev

webspresso new flows

Development server

webspresso dev expects a server.js in the project root, uses Node’s watch mode on pages, models, views when present, and can run Tailwind watch:css alongside.

webspresso dev
webspresso dev --port 3001
webspresso dev --no-css   # skip CSS watch if Tailwind is configured

Project structure

Typical application layout (database pieces appear after you opt in or add them manually):

my-app/
├── pages/
│   ├── locales/              # global i18n (en.json, de.json, …)
│   ├── _hooks.js             # global lifecycle hooks
│   ├── index.njk             # GET /
│   ├── about/
│   │   ├── index.njk
│   │   └── locales/          # route-level overrides
│   ├── tools/
│   │   ├── index.njk
│   │   ├── index.js          # load(), meta(), middleware, hooks
│   │   ├── [slug].njk
│   │   └── [slug].js
│   └── api/
│       ├── health.get.js
│       └── echo.post.js
├── views/
│   └── layout.njk
├── public/                   # static assets
├── models/                   # optional ORM models
├── migrations/               # Knex migrations
├── seeds/                    # optional seed entry (seeds/index.js)
├── webspresso.db.js          # optional Knex config
└── server.js                 # createApp() entry

Environment variables

VariableDefaultDescription
NODE_ENVdevelopmentControls logging, plugin defaults (e.g. dashboard)
DEFAULT_LOCALEenFallback locale
SUPPORTED_LOCALESenComma-separated list (e.g. en,de)
BASE_URLhttp://localhost:3000Canonical URLs, sitemap, metadata
DATABASE_URLConnection string for Knex / ORM

webspresso start sets NODE_ENV=production and honors PORT via the --port flag.

CLI commands

Entry: webspresso (bin/webspresso.js). Use --help on any subcommand for flags.

CommandDescription
new [project-name]Scaffold a project (Tailwind, i18n, scripts; optional DB + seeds)
pageInteractive: add SSR page + optional route config + locales
apiInteractive: add API route + HTTP method
devDevelopment server (Node watch, optional CSS watch)
startProduction server via server.js
add tailwindAdd Tailwind, PostCSS, build + watch scripts
db:migrateRun pending migrations
db:rollbackRollback last migration batch
db:statusShow migration status
db:make <name>Create migration file (optional --model scaffold)
seedRun seeds/index.js (Faker-based fake data)
admin:setupEmit admin_users migration for admin plugin
admin:listList admin users (needs migrated admin_users)
admin:passwordReset admin password (interactive or -e / -p)
audit:pruneDelete audit log rows older than --days (optional --table)
doctorSanity checks: Node version vs package.json engines, expected project files, optional DB ping — run from your app root
skill [name]Create an Agent Skill (SKILL.md) for Cursor and similar tools — interactive name/description, or use --preset webspresso to install the bundled Webspresso reference skill under .cursor/skills/
favicon:generate <source>PNG favicons, PWA manifest, Nunjucks partial

Selected flags

CommandFlags / notes
new-i, --install · --no-tailwind
dev / start-p, --port (default 3000) · dev --no-css
seed--setup · --config · --env
admin:password / admin:list-c, --config · -E, --env
favicon:generate-o · --no-layout · PWA --name / --short-name / --theme-color
doctor--db (test DB when webspresso.db.js / knexfile.js exists) · --strict (exit 1 on warnings) · -e, --env (with --db)
skill-g, --global (write to ~/.cursor/skills/) · -d, --description · -f, --force · -p, --preset <name> (e.g. webspresso)

API — createApp(options)

Returns { app, nunjucksEnv, pluginManager, authMiddleware } (auth helper may be no-op if auth is not configured). Express is preconfigured with sensible security headers (Helmet), sessions when needed, static files, and the file router.

OptionRole
pagesDirRequired. Root of routes and templates resolution.
viewsDirNunjucks views / layouts.
publicDirStatic files (default public).
dbORM instance → ctx.db (SSR), req.db on pages/api (before middleware); getDb() / attachDbMiddleware elsewhere.
pluginsArray of plugin factories or objects.
middlewaresNamed map referenced from route configs (middleware: ['auth']).
helmettrue / false / custom Helmet options object.
loggingHTTP logging (on by default in development).
timeoutString duration ('30s') or false — uses connect-timeout.
errorPagesnotFound, serverError, timeout — handler fn or template path.
assetsVersion string or manifest path for cache-busted fsy.asset() URLs.
const { createApp } = require('webspresso');

const { app } = createApp({
  pagesDir: './pages',
  viewsDir: './views',
  middlewares: {
    auth: (req, res, next) => {
      if (!req.session?.user) return res.redirect('/login');
      next();
    },
  },
  errorPages: {
    notFound: 'errors/404.njk',
    serverError: 'errors/500.njk',
  },
});

File-based routing

SSR pages

File pathRoute
pages/index.njkGET /
pages/about/index.njkGET /about
pages/tools/[slug].njkGET /tools/:slug
pages/docs/[...rest].njkGET /docs/*

API routes

File pathRoute
pages/api/health.get.jsGET /api/health
pages/api/echo.post.jsPOST /api/echo
pages/api/users/[id].get.jsGET /api/users/:id

Route config (pages/.../*.js next to .njk)

module.exports = {
  middleware: ['auth'],
  async load(req, ctx) {
    const posts = await ctx.db.getRepository('Post').query().limit(10).list();
    return { posts };
  },
  meta(req, ctx) {
    return { title: 'Blog', description: 'Latest posts' };
  },
  hooks: { beforeLoad: async () => {}, afterRender: async () => {} },
};

API module shapes

Per request: req.db (if configured) → Zod schemamiddlewarehandler.

module.exports = {
  middleware: ['requireAuth'],
  schema: ({ z }) => ({ body: z.object({ q: z.string() }) }),
  handler: async (req, res) => {
    return res.json({ q: req.input.body.q, results: [] });
  },
};

Zod schemas (API)

KeyValidates
bodyPOST / PUT / PATCH JSON body
paramsPath params (:id, …)
queryQuery string
responseDocumentation only (not enforced at runtime)

Validated values are on req.input; failures return 400 JSON { error: 'Validation Error', issues }.

Plugin system

Plugins can register Express middleware, template helpers/filters, routes in onRoutesReady, and expose an api object for other plugins. Errors during registration generally log a warning instead of crashing the process.

Built-in (require path)Purpose
webspresso/pluginsdashboardPluginDev-only route browser at /_webspresso
sitemapPlugin/sitemap.xml, /robots.txt, optional DB-driven URLs
analyticsPluginGA4, GTM, Yandex, Bing UET, Facebook Pixel, verification meta tags
siteAnalyticsPluginSelf-hosted page views + admin charts
adminPanelPluginCRUD admin SPA (Mithril), auth hooks, custom modules
seoCheckerPluginDev toolbar SEO audit (40+ checks)
schemaExplorerPluginJSON schema of models + ORM components OpenAPI export
swaggerPluginOpenAPI 3 for pages/api + Zod; Swagger UI (dev by default)
healthCheckPluginGET /health probe (optional DB checks, custom path)

ORM & database

The ORM layers Knex with Zod: column metadata lives on Zod schemas via zdb helpers (zdb.id(), zdb.uuid(), zdb.nanoid(), zdb.string(), foreign keys, relations, soft deletes, …). Repositories provide findById, query() with pagination, transactions, and migrations via the same Knex instance.

Schema helpers (zdb)

Helpers wrap Zod with database column metadata (same surface as README — Schema helpers).

HelperDescriptionOptions
zdb.id()Primary key (bigint, auto-increment)
zdb.uuid()UUID primary key
zdb.nanoid(opts)Nanoid primary key (URL-safe string, VARCHAR)maxLength (default 21)
zdb.string(opts)VARCHAR columnmaxLength, unique, index, nullable
zdb.text(opts)TEXT columnnullable
zdb.integer(opts)INTEGER columnnullable, default
zdb.bigint(opts)BIGINT columnnullable
zdb.float(opts)FLOAT columnnullable
zdb.decimal(opts)DECIMAL columnprecision, scale, nullable
zdb.boolean(opts)BOOLEAN columndefault, nullable
zdb.date(opts)DATE columnnullable
zdb.datetime(opts)DATETIME columnnullable
zdb.timestamp(opts)TIMESTAMP columnauto: 'create'|'update', nullable
zdb.json(opts)JSON columnnullable
zdb.array(itemSchema, opts)ARRAY column (stored as JSON)nullable
zdb.enum(values, opts)ENUM columndefault, nullable
zdb.foreignKey(table, opts)Foreign key (bigint)referenceColumn, nullable
zdb.foreignUuid(table, opts)Foreign key (uuid)referenceColumn, nullable
zdb.foreignNanoid(table, opts)Foreign key (nanoid string)referenceColumn, nullable, maxLength (match referenced PK)

Nanoid columns: migration scaffolding uses table.string(column, maxLength). For a nanoid primary key, omitting the PK on repository.create() fills it with a cryptographically random ID (same default alphabet as the nanoid package; built into Webspresso, no extra npm dependency). Use generateNanoid from webspresso when you need the same generator manually. For API schema validation (params, query, body), use z.nanoid(), z.nanoid(n), or z.nanoid({ maxLength }) on the z from your route schema (or zodNanoid / extendZ outside compiled routes).

const { zdb, defineModel, createDatabase } = require('webspresso');

const User = defineModel({
  name: 'User',
  table: 'users',
  schema: zdb.schema({
    id: zdb.id(),
    email: zdb.string({ unique: true }),
    created_at: zdb.timestamp({ auto: 'create' }),
  }),
});

const db = createDatabase({
  client: 'pg',
  connection: process.env.DATABASE_URL,
  models: './models',
});
npm install pg mysql2 better-sqlite3   # pick one
webspresso db:migrate
webspresso db:make add_posts_table --model Post
webspresso seed

Migrations live under migrations/ as usual for Knex; webspresso.db.js (or knexfile.js) is loaded for all DB CLI commands.

i18n & template helpers

Locales merge: global JSON in pages/locales/, then route-specific folders override keys for that subtree.

{
  "nav": { "home": "Home", "about": "About" }
}
<h1>{{ t('nav.home') }}</h1>
{{ fsy.canonical() | safe }}
{{ fsy.url('/blog', { page: 2 }) }}

fsy groups include URL builders, request accessors, slugify/truncate/pretty bytes, dayjs-powered dates, dev flag, JSON-LD helper, and asset tags when assets is configured — see README for the exhaustive list.

Lifecycle hooks

Global hook module:

// pages/_hooks.js
module.exports = {
  onRequest(ctx) {},
  beforeLoad(ctx) {},
  afterLoad(ctx) {},
  beforeRender(ctx) {},
  afterRender(ctx) {},
  onError(ctx, err) {},
};

Execution order (SSR)

  1. Global onRequest → route onRequest
  2. beforeMiddleware → middleware chain → afterMiddleware
  3. beforeLoadloadafterLoad
  4. beforeRender → Nunjucks render → afterRender

Tooling & developer experience

Webspresso bundles the pieces you usually wire by hand: a CLI for scaffolding and DB tasks, a dev runner that restarts the process when routes or templates change, and optional first-party UI for routes and SEO. Below is how those tools fit together.

CLI & project automation

The webspresso binary (Commander) exposes subcommands for projects, APIs, Tailwind, Knex migrations, seeds, admin maintenance, audit pruning, environment checks (doctor), Cursor Agent Skills (skill, including a bundled Webspresso preset), and favicon/PWA asset generation. Interactive flows use Inquirer when you run commands without full arguments (e.g. webspresso new with no name).

Local development server

webspresso dev runs your project’s server.js under Node’s --watch and adds --watch-path for pages/, models/, and views/ when those directories exist, so route files and Nunjucks layouts reload without manual restarts. With Tailwind enabled, the same command can spawn watch:css; use --no-css to skip that subprocess.

CSS & front-end pipeline

Scaffolded apps use Tailwind CSS with PostCSS and Autoprefixer (webspresso add tailwind or the default new template). Compiled CSS is written under public/css/ and linked from the layout. For cache-busted assets in SSR, createApp({ assets }) supports a static version string or a Vite/Webpack-style manifest so fsy.asset() resolves hashed filenames.

In-browser dev tools

Enable dashboardPlugin() to get a route inventory at /_webspresso (development only by default). The SEO checker plugin adds a dev-only panel with many automated HTML/metadata checks. These tools stay out of production unless you explicitly force them on.

Images & PWA assets

webspresso favicon:generate uses sharp to resize a master PNG into Apple/Android/favicon sizes, writes a manifest.json, and can inject a Nunjucks partial into your layout — useful for consistent branding across devices.

Request flow (conceptual)

Incoming HTTP traffic passes through Express middleware registered by the framework and by plugins, then the file router decides whether the request targets an API module under pages/api or an SSR page. API handlers may run Zod first; SSR routes run load() then Nunjucks with globals (fsy, t, …).

  Client
    │
    ▼
┌─────────────────────────────────────────┐
│ Express stack: cookie/session, helmet,  │
│ body parsers, timeout, static files     │
│ Plugin register(ctx) — extend app       │
└─────────────────┬───────────────────────┘
                  ▼
         mountPages (file-router)
                  │
      ┌───────────┴───────────┐
      ▼                       ▼
 pages/api/*.js          pages/*.njk + route *.js
 Zod → handler           load() → Nunjucks → HTML
      │                       │
      └───────────┬───────────┘
                  ▼
            HTTP response

Architecture

Two layers matter: your application (a small server.js that calls createApp, plus pages/, views/, optional models/) and the webspresso package, which implements routing, rendering, plugins, and ORM primitives. The public API surface is re-exported from index.js so consumers rarely import deep paths.

index.js — public exports

Export areaExamples
AppcreateApp
Router utilitiesmountPages, filePathToRoute, scanDirectory, i18n helpers
Templates & assetscreateHelpers, AssetManager, configureAssets
PluginsPluginManager, createPluginManager
ORMdefineModel, createDatabase, zdb, generateNanoid, zodNanoid, …
Convenience pluginsschemaExplorerPlugin, swaggerPlugin, healthCheckPlugin, adminPanelPlugin, siteAnalyticsPlugin, auditLogPlugin, recaptchaPlugin

src/server.jscreateApp pipeline

  1. Build the Express app and apply Helmet (CSP enabled in production; relaxed in dev for hot editing).
  2. Attach sessions, parsers, connect-timeout when configured, and static file serving.
  3. Instantiate PluginManager, run each plugin’s register(ctx) (templates, middleware, helpers).
  4. Configure Nunjucks with configureAssets / createHelpers for fsy.
  5. Call mountPages from file-router.js to register SSR and API routes from disk.
  6. Run onRoutesReady on plugins so they can add routes or read the route table.
  7. Invoke onReady when the server starts listening.

Source modules (published package)

PathResponsibility
src/server.jscreateApp, Express stack, error pages, plugin orchestration
src/file-router.jsScan pages/, map files to routes, wire API handlers and SSR
src/helpers.jsNunjucks globals (fsy), asset URL resolution
src/plugin-manager.jsPlugin registry, dependency metadata, lifecycle hooks
core/orm/Models, repositories, Knex query builder, migrations, seeding
core/auth/Session-aware helpers for protected routes / admin
core/compileSchema.js / applySchema.jsZod schema compilation for validation layers
plugins/Built-in dashboard, sitemap, analytics, admin, SEO checker, …
utils/Shared helpers (e.g. schema cache)
bin/CLI commands (Commander)

Package layout (package.json files)

index.js           # re-exports createApp, router utils, ORM, plugins
bin/               # webspresso CLI
core/              # ORM, auth, Zod compile/apply
plugins/           # first-party plugins
src/               # server.js, file-router, helpers, plugin-manager
utils/             # shared utilities

Development & testing

The framework repository runs Vitest for fast unit and integration tests (CLI, ORM, routing, plugins, etc.) and Playwright for end-to-end checks in a real browser: admin panel APIs and UI, auth flows, audit log, CLI project scaffold, and the SEO checker plugin.

Unit & integration (Vitest)

npm test                 # vitest run
npm run test:watch
npm run test:coverage

End-to-end (Playwright)

Specs live under tests/e2e/; the runner starts a temporary app and exercises HTTP + DOM (Chromium by default). Use UI or headed mode when debugging flaky selectors.

npm run test:e2e           # playwright test
npm run test:e2e:ui      # Playwright UI
npm run test:e2e:debug
npm run test:e2e:headed

CI tip: run npm test and npm run test:e2e before release. For a quick local health pass on an app directory, use webspresso doctor (add --db to verify database connectivity).