webspresso
v0.0.46
Published
Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling
Maintainers
Readme
Webspresso
A minimal, file-based SSR framework for Node.js with Nunjucks templating.
Features
- File-Based Routing: Create pages by adding
.njkfiles to apages/directory - Dynamic Routes: Use
[param]for dynamic params and[...rest]for catch-all routes - API Endpoints: Add
.jsfiles topages/api/with method suffixes (e.g.,health.get.js) - Schema Validation: Zod-based request validation for body, params, and query
- Built-in i18n: JSON-based translations with automatic locale detection
- Lifecycle Hooks: Global and route-level hooks for request processing
- Template Helpers: Laravel-inspired helper functions available in templates
- Plugin System: Extensible architecture with version control and inter-plugin communication
- Built-in Plugins: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics
Installation
npm install -g webspresso
# or
npm install webspressoQuick Start
Using CLI (Recommended)
# Create a new project (Tailwind CSS included by default)
webspresso new my-app
# Navigate to project
cd my-app
# Install dependencies
npm install
# Build Tailwind CSS
npm run build:css
# Start development server (watches both CSS and server)
webspresso dev
# or
npm run devNote: New projects include Tailwind CSS by default. Use
--no-tailwindflag to skip it.
CLI Commands
webspresso new [project-name]
Create a new Webspresso project with Tailwind CSS (default).
# Create in a new directory
webspresso new my-app
# Create in current directory (interactive)
webspresso new
# → Prompts: "Install in current directory?"
# → If yes, asks for project name (for package.json)
# Auto install dependencies and build CSS
webspresso new my-app --install
# Without Tailwind
webspresso new my-app --no-tailwindInteractive Mode (no arguments):
- Asks if you want to install in the current directory
- If current directory is not empty, shows a warning
- Prompts for project name (defaults to current folder name)
- Asks if you will use a database (SQLite, PostgreSQL, or MySQL)
- If yes, adds the appropriate driver to
package.jsonand createswebspresso.db.jsconfig - After project creation, asks if you want to install dependencies
- If yes, runs
npm installandnpm run build:css - Then asks if you want to start the development server
- If yes, starts
npm run devautomatically
Auto Installation:
# With --install flag (semi-interactive)
webspresso new my-app --install
# → Automatically runs: npm install && npm run build:css
# → Then prompts: "Start development server?" [Y/n]
# → If yes: starts npm run dev (with watch:css if Tailwind enabled)
# Without --install flag (fully interactive)
webspresso new my-app
# → Prompts: "Install dependencies and build CSS now?" [Y/n]
# → If yes: runs npm install && npm run build:css
# → Then: "Start development server?" [Y/n]
# → If yes: starts npm run dev (with watch:css if Tailwind enabled)Note: When dev server starts with Tailwind CSS, it automatically runs watch:css in the background to watch for CSS changes.
Database Selection: During project creation, you'll be asked if you want to use a database:
- SQLite (better-sqlite3) - Recommended for development and small projects
- PostgreSQL (pg) - For production applications
- MySQL (mysql2) - Alternative SQL database
If you select a database:
- The appropriate driver is added to
package.jsondependencies webspresso.db.jsconfig file is created with proper settingsmigrations/directory is createdmodels/directory is createdDATABASE_URLis added to.env.examplewith a template
Seed Data Generation: After selecting a database, you'll be asked if you want to generate seed data:
- If yes,
@faker-js/fakeris added to dependencies seeds/directory is created withseeds/index.jsnpm run seedscript is added topackage.json- The seed script automatically detects models in
models/directory and generates fake data based on their schemas
To run seeds after creating models:
npm run seedThe seed script will:
- Load all models from
models/directory - Generate 10 fake records per model (by default)
- Use smart field detection based on column names (email, name, title, etc.)
You can always add database support later by:
- Installing the driver:
npm install better-sqlite3(orpg,mysql2) - Creating
webspresso.db.jsconfig file - Adding
DATABASE_URLto your.envfile - Creating
models/directory and defining your models - Optionally adding seed support:
npm install @faker-js/fakerand creatingseeds/index.js
Options:
-i, --install- Auto runnpm installandnpm run build:css(non-interactive)--no-tailwind- Skip Tailwind CSS setup
The project includes:
- Tailwind CSS with build process
- Optimized layout template with navigation and footer
- Responsive starter page
- i18n setup (en/tr)
- Development and production scripts
webspresso page
Add a new page to your project (interactive prompt).
webspresso pageThe CLI will ask you:
- Route path (e.g.,
/aboutor/blog/post) - Whether to add a route config file
- Whether to add locale files
webspresso api
Add a new API endpoint (interactive prompt).
webspresso apiThe CLI will ask you:
- API route path (e.g.,
/api/usersor/api/users/[id]) - HTTP method (GET, POST, PUT, PATCH, DELETE)
webspresso dev
Start development server with hot reload.
webspresso dev
# or with custom port
webspresso dev --port 3001webspresso start
Start production server.
webspresso start
# or with custom port
webspresso start --port 3000webspresso add tailwind
Add Tailwind CSS to your project with build process.
webspresso add tailwindThis command will:
- Install Tailwind CSS, PostCSS, and Autoprefixer as dev dependencies
- Create
tailwind.config.jsandpostcss.config.js - Create
src/input.csswith Tailwind directives - Add build scripts to
package.json - Update your layout to use the built CSS instead of CDN
- Create
public/css/style.cssfor the compiled output
After running this command:
npm install
npm run build:css # Build CSS once
npm run watch:css # Watch and rebuild CSS on changes
npm run dev # Starts both CSS watch and dev serverProject Structure
Create your app with this structure:
my-app/
├── pages/
│ ├── locales/ # Global i18n translations
│ │ ├── en.json
│ │ └── tr.json
│ ├── _hooks.js # Global lifecycle hooks
│ ├── index.njk # Home page (GET /)
│ ├── about/
│ │ ├── index.njk # About page (GET /about)
│ │ └── locales/ # Route-specific translations
│ ├── tools/
│ │ ├── index.njk # Tools list (GET /tools)
│ │ ├── index.js # Route config with load()
│ │ ├── [slug].njk # Dynamic tool page (GET /tools/:slug)
│ │ └── [slug].js # Route config for dynamic page
│ └── api/
│ ├── health.get.js # GET /api/health
│ └── echo.post.js # POST /api/echo
├── views/
│ └── layout.njk # Base layout template
├── public/ # Static files
└── server.jsAPI
createApp(options)
Creates and configures the Express app.
Options:
pagesDir(required): Path to pages directoryviewsDir(optional): Path to views/layouts directorypublicDir(optional): Path to public/static directorylogging(optional): Enable request logging (default: true in development)helmet(optional): Helmet security configurationtrueorundefined: Use default secure configurationfalse: Disable HelmetObject: Custom Helmet configuration (merged with defaults)
middlewares(optional): Named middleware registry for routes
Example with middlewares:
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();
},
admin: (req, res, next) => {
if (req.session?.user?.role !== 'admin') {
return res.status(403).send('Forbidden');
}
next();
},
rateLimit: require('express-rate-limit')({ windowMs: 60000, max: 100 })
}
});Then use in route configs by name:
// pages/admin/index.js
module.exports = {
middleware: ['auth', 'admin'], // Use named middlewares
load(req, ctx) { ... }
};
// pages/api/data.get.js
module.exports = {
middleware: ['auth', 'rateLimit'],
handler: (req, res) => res.json({ data: 'protected' })
};Custom Error Pages:
const { createApp } = require('webspresso');
const { app } = createApp({
pagesDir: './pages',
viewsDir: './views',
errorPages: {
// Option 1: Custom handler function
notFound: (req, res) => {
res.render('errors/404.njk', { url: req.url });
},
// Option 2: Template path (rendered with Nunjucks)
serverError: 'errors/500.njk',
// Timeout error page (503)
timeout: 'errors/503.njk'
}
});Error templates receive these variables:
404.njk:{ url, method }500.njk:{ error, status, isDev }503.njk:{ url, method, isDev }
Request Timeout:
Configure request timeout with connect-timeout:
const { app } = createApp({
pagesDir: './pages',
timeout: '30s', // Default: 30 seconds
// timeout: '1m', // 1 minute
// timeout: false, // Disable timeout
});Asset Management:
Configure asset handling with versioning and manifest support:
const { createApp } = require('webspresso');
const path = require('path');
const { app } = createApp({
pagesDir: './pages',
viewsDir: './views',
publicDir: './public',
assets: {
// Option 1: Simple versioning (cache busting)
version: '1.2.3', // or process.env.APP_VERSION
// Option 2: Manifest file (Vite, Webpack, etc.)
manifestPath: path.join(__dirname, 'public/.vite/manifest.json'),
// URL prefix for assets
prefix: '/static'
}
});Use asset helpers in templates:
{# Using fsy helpers (auto-resolved) #}
<link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
{# Or generate full HTML tags #}
{{ fsy.css('/css/style.css') | safe }}
{{ fsy.js('/js/app.js', { defer: true, type: 'module' }) | safe }}
{{ fsy.img('/images/logo.png', 'Site Logo', { class: 'logo', loading: 'lazy' }) | safe }}Asset helpers available in fsy:
asset(path)- Returns versioned/manifest-resolved URLcss(href, attrs)- Generates<link>tagjs(src, attrs)- Generates<script>tagimg(src, alt, attrs)- Generates<img>tag
Manifest Support:
Works with Vite and Webpack manifest formats:
// Vite manifest format (.vite/manifest.json)
{
"css/style.css": { "file": "assets/style-abc123.css" },
"js/app.js": { "file": "assets/app-xyz789.js" }
}
// Webpack manifest format
{
"/css/style.css": "/dist/style.abc123.css",
"/js/app.js": "/dist/app.xyz789.js"
}Returns: { app, nunjucksEnv, pluginManager }
Plugin System
Webspresso has a built-in plugin system with version control and dependency management.
Using Plugins
const { createApp } = require('webspresso');
const { sitemapPlugin, analyticsPlugin, dashboardPlugin } = require('webspresso/plugins');
const { app } = createApp({
pagesDir: './pages',
viewsDir: './views',
plugins: [
dashboardPlugin(), // Dev dashboard at /_webspresso
sitemapPlugin({
hostname: 'https://example.com',
exclude: ['/admin/*', '/api/*'],
i18n: true,
locales: ['en', 'tr']
}),
analyticsPlugin({
google: {
measurementId: 'G-XXXXXXXXXX',
verificationCode: 'xxxxx'
},
yandex: {
counterId: '12345678',
verificationCode: 'xxxxx'
},
bing: {
uetId: '12345678',
verificationCode: 'xxxxx'
}
})
]
});Built-in Plugins
Dashboard Plugin:
- Development dashboard at
/_webspresso - Monitor all routes (SSR pages and API endpoints)
- View loaded plugins and configuration
- Filter and search routes
- Only active in development mode (disabled in production)
const { dashboardPlugin } = require('webspresso/plugins');
const { app } = createApp({
pagesDir: './pages',
plugins: [
dashboardPlugin() // Available at /_webspresso in dev mode
]
});Options:
path- Custom dashboard path (default:/_webspresso)enabled- Force enable/disable (default: auto based on NODE_ENV)
Sitemap Plugin:
- Generates
/sitemap.xmlfrom routes automatically - Dynamic Database Content: Generate URLs from database records
- Excludes dynamic routes and API endpoints
- Supports i18n with hreflang tags
- Generates
/robots.txt - Configurable caching for performance
sitemapPlugin({
hostname: 'https://example.com',
db, // Database instance
dynamicSources: [
{
model: 'Post', // Model name
urlPattern: '/blog/:slug', // URL pattern
lastmodField: 'updated_at', // Field for lastmod
filter: (r) => r.published, // Filter records
priority: 0.9,
},
{
// Custom query function
query: async (db) => {
return db.getRepository('Product')
.query()
.where('active', true)
.list();
},
urlPattern: '/products/:slug',
},
],
})Analytics Plugin:
- Google Analytics (GA4) and Google Ads
- Google Tag Manager
- Yandex.Metrika
- Microsoft/Bing UET
- Facebook Pixel
- Verification meta tags for all services
Template helpers from analytics plugin:
<head>
{{ fsy.verificationTags() | safe }}
{{ fsy.analyticsHead() | safe }}
</head>
<body>
{{ fsy.analyticsBodyOpen() | safe }}
...
</body>Individual helpers: gtag(), gtm(), gtmNoscript(), yandexMetrika(), bingUET(), facebookPixel(), allAnalytics()
Site Analytics Plugin:
- Self-hosted page view analytics (no external services required)
- Automatic page view tracking via Express middleware
- Bot detection (40+ patterns: Googlebot, GPTBot, curl, etc.)
- Country detection (CDN headers, Accept-Language fallback)
- Admin panel dashboard with Chart.js visualizations
- Privacy-first: IP addresses are hashed, no cookies required
const { siteAnalyticsPlugin, adminPanelPlugin } = require('webspresso/plugins');
const { app } = createApp({
pagesDir: './pages',
plugins: [
adminPanelPlugin({ db }),
siteAnalyticsPlugin({
db,
excludePaths: ['/health', '/favicon.ico'],
trackBots: true, // Record bot visits separately (default: true)
}),
]
});Admin panel analytics page includes:
- Summary cards: Total views, unique visitors, unique pages, sessions
- Views over time: Line chart (Chart.js) with daily views/visitors/sessions
- Bot activity: Bot request counts with horizontal bar visualization
- Top pages: Most viewed pages sorted by view count
- Recent activity: Latest page views with country flags and timestamps
- Country stats: Country breakdown with flag emojis and bar charts
- Date filtering: Last 7, 30, or 90 days toggle
Options:
db(required) - Database instanceexcludePaths- Additional paths to exclude from tracking (admin, API, and static files are auto-excluded)trackBots- Whether to record bot visits (default:true)tableName- Custom table name (default:analytics_page_views)
The analytics_page_views table is automatically created on first request.
SEO Checker Plugin:
- Client-side SEO analysis tool (inspired by django-check-seo)
- Integrated with dev toolbar
- 40+ SEO checks across 7 categories
- Real-time analysis with score calculation
- Only active in development mode
const { seoCheckerPlugin } = require('webspresso/plugins');
const { app } = createApp({
pagesDir: './pages',
plugins: [
seoCheckerPlugin({
settings: {
titleLength: [30, 60], // Min/max title length
descriptionLength: [50, 160], // Min/max description length
minContentWords: 300, // Minimum content words
minInternalLinks: 1, // Minimum internal links
minExternalLinks: 1, // Minimum external links
maxUrlLength: 75, // Maximum URL length
maxUrlDepth: 3 // Maximum URL depth
}
})
]
});SEO Check Categories: | Category | Checks | |----------|--------| | Meta | Title, Description, Canonical, Viewport, Robots, Charset, Lang | | Headings | H1 existence, Single H1, Hierarchy, Non-empty headings | | Content | Word count, Paragraphs, Keyword usage, Keywords early | | Links | Internal links, External links, Nofollow, Anchor text | | Images | Alt text, Descriptive alt, Dimensions, Lazy loading | | Structured | Open Graph, Twitter Card, JSON-LD, Hreflang | | URL | Length, Depth, Readability, HTTPS |
The SEO Checker panel appears as a floating widget and can be opened via:
- Dev toolbar "SEO Check" button
- Floating toggle button (🔍) in bottom-right corner
Creating Custom Plugins
const myPlugin = {
name: 'my-plugin',
version: '1.0.0',
// Optional: depend on other plugins
dependencies: {
'analytics': '^1.0.0'
},
// Optional: expose API for other plugins
api: {
getData() { return this.data; }
},
// Called during registration
register(ctx) {
// Access Express app
ctx.app.use((req, res, next) => next());
// Add template helpers
ctx.addHelper('myHelper', () => 'Hello!');
// Add Nunjucks filters
ctx.addFilter('myFilter', (val) => val.toUpperCase());
// Use other plugins
const analytics = ctx.usePlugin('analytics');
},
// Called after all routes are mounted
onRoutesReady(ctx) {
// Access route metadata
console.log('Routes:', ctx.routes);
// Add custom routes
ctx.addRoute('get', '/my-route', (req, res) => {
res.json({ hello: 'world' });
});
},
// Called before server starts
onReady(ctx) {
console.log('Server ready!');
}
};
// Use as factory function for configuration
function myPluginFactory(options = {}) {
return {
name: 'my-plugin',
version: '1.0.0',
_options: options,
register(ctx) {
// ctx.options contains the passed options
}
};
}File-Based Routing
SSR Pages
Create .njk files in the pages/ directory:
| File Path | Route |
|-----------|-------|
| pages/index.njk | / |
| pages/about/index.njk | /about |
| pages/tools/[slug].njk | /tools/:slug |
| pages/docs/[...rest].njk | /docs/* |
API Routes
Create .js files in pages/api/ with optional method suffixes:
| File Path | Route |
|-----------|-------|
| pages/api/health.get.js | GET /api/health |
| pages/api/echo.post.js | POST /api/echo |
| pages/api/users/[id].get.js | GET /api/users/:id |
Basic API Handler:
// pages/api/health.get.js
module.exports = async function handler(req, res) {
res.json({ status: 'ok' });
};With Schema Validation:
// pages/api/posts.post.js
module.exports = {
schema: ({ z }) => ({
body: z.object({
title: z.string().min(3).max(100),
content: z.string(),
tags: z.array(z.string()).optional()
}),
query: z.object({
draft: z.coerce.boolean().default(false)
})
}),
async handler(req, res) {
// Validated & parsed data available in req.input
const { title, content, tags } = req.input.body;
const { draft } = req.input.query;
// Original req.body, req.query remain untouched
res.json({ success: true, title, draft });
}
};Schema Options:
| Key | Description |
|-----|-------------|
| body | Validates req.body (POST/PUT/PATCH) |
| params | Validates route parameters (e.g., :id) |
| query | Validates query string parameters |
| response | Response schema (for documentation, not enforced) |
All schemas use Zod for validation. Invalid requests throw a ZodError which can be caught by error handlers.
Dynamic Route with Params Validation:
// pages/api/users/[id].get.js
module.exports = {
schema: ({ z }) => ({
params: z.object({
id: z.string().uuid()
}),
query: z.object({
fields: z.string().optional()
})
}),
async handler(req, res) {
const { id } = req.input.params; // Validated UUID
const user = await getUser(id);
res.json(user);
}
};Route Config
Add a .js file alongside your .njk file to configure the route:
// pages/tools/index.js
module.exports = {
// Middleware for this route
middleware: [(req, res, next) => next()],
// Load data for SSR
async load(req, ctx) {
return { tools: await fetchTools() };
},
// Override meta tags
meta(req, ctx) {
return {
title: 'Tools',
description: 'Developer tools'
};
},
// Route-level hooks
hooks: {
beforeLoad: async (ctx) => {},
afterRender: async (ctx) => {}
}
};i18n
Global Translations
Add JSON files to pages/locales/:
{
"nav": {
"home": "Home",
"about": "About"
}
}Route-Specific Translations
Add a locales/ folder inside any route directory to override global translations.
Using Translations
In templates:
<h1>{{ t('nav.home') }}</h1>Template Helpers
The fsy object is available in all templates:
{# URL helpers #}
{{ fsy.url('/tools', { page: 1 }) }}
{{ fsy.fullUrl('/tools') }}
{{ fsy.route('/tools/:slug', { slug: 'test' }) }}
{# Request helpers #}
{{ fsy.q('page', 1) }}
{{ fsy.param('slug') }}
{{ fsy.hdr('User-Agent') }}
{# Utility helpers #}
{{ fsy.slugify('Hello World') }}
{{ fsy.truncate(text, 100) }}
{{ fsy.prettyBytes(1024) }}
{{ fsy.prettyMs(5000) }}
{# Date/Time helpers (dayjs) #}
{{ fsy.dateFormat(post.created_at, 'YYYY-MM-DD HH:mm') }}
{{ fsy.dateFromNow(post.created_at) }} {# "2 hours ago" #}
{{ fsy.dateAgo(post.created_at) }} {# "2 hours ago" #}
{{ fsy.dateUntil(event.date) }} {# "in 2 hours" #}
{{ fsy.date(post.created_at).format('MMMM D, YYYY') }} {# Full dayjs API #}
{% if fsy.dateIsBefore(post.created_at, fsy.date()) %}Published{% endif %}
{{ fsy.dateDiff(post.created_at, fsy.date(), 'day') }} days ago
{{ fsy.dateAdd(post.created_at, 7, 'day').format('YYYY-MM-DD') }}
{{ fsy.dateStartOf(post.created_at, 'month').format('YYYY-MM-DD') }}
{# Environment #}
{% if fsy.isDev() %}Dev mode{% endif %}
{# SEO #}
{{ fsy.canonical() }}
{{ fsy.jsonld(schema) | safe }}Lifecycle Hooks
Global Hooks
Create pages/_hooks.js:
module.exports = {
onRequest(ctx) {},
beforeLoad(ctx) {},
afterLoad(ctx) {},
beforeRender(ctx) {},
afterRender(ctx) {},
onError(ctx, err) {}
};Hook Execution Order
- Global
onRequest - Route
onRequest - Route
beforeMiddleware - Route middleware
- Route
afterMiddleware - Route
beforeLoad - Route
load() - Route
afterLoad - Route
beforeRender - Nunjucks render
- Route
afterRender
Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| NODE_ENV | development | Environment |
| DEFAULT_LOCALE | en | Default locale |
| SUPPORTED_LOCALES | en | Comma-separated locales |
| BASE_URL | http://localhost:3000 | Base URL for canonical URLs |
| DATABASE_URL | - | Database connection string (for ORM) |
ORM (Database)
Webspresso includes a minimal, Eloquent-inspired ORM built on Knex with Zod schemas as the single source of truth.
Quick Start
const { zdb, defineModel, createDatabase } = require('webspresso');
// 1. Define your schema with database metadata
const UserSchema = zdb.schema({
id: zdb.id(),
email: zdb.string({ unique: true, index: true }),
name: zdb.string({ maxLength: 100 }),
status: zdb.enum(['active', 'inactive'], { default: 'active' }),
company_id: zdb.foreignKey('companies', { nullable: true }),
created_at: zdb.timestamp({ auto: 'create' }),
updated_at: zdb.timestamp({ auto: 'update' }),
deleted_at: zdb.timestamp({ nullable: true }),
});
// 3. Define your model
const User = defineModel({
name: 'User',
table: 'users',
schema: UserSchema,
relations: {
company: { type: 'belongsTo', model: () => Company, foreignKey: 'company_id' },
posts: { type: 'hasMany', model: () => Post, foreignKey: 'user_id' },
},
scopes: { softDelete: true, timestamps: true },
});
// 4. Create database (models auto-loaded from ./models directory)
const db = createDatabase({
client: 'pg',
connection: process.env.DATABASE_URL,
models: './models', // Optional, defaults to './models'
});
// Models are automatically loaded from models/ directory
// Use getRepository with model name
const UserRepo = db.getRepository('User');
const user = await UserRepo.findById(1, { with: ['company', 'posts'] });Schema Helpers (zdb)
The zdb helpers wrap Zod schemas with database column metadata:
| Helper | Description | Options |
|--------|-------------|---------|
| zdb.id() | Primary key (bigint, auto-increment) | |
| zdb.uuid() | UUID primary key | |
| zdb.string(opts) | VARCHAR column | maxLength, unique, index, nullable |
| zdb.text(opts) | TEXT column | nullable |
| zdb.integer(opts) | INTEGER column | nullable, default |
| zdb.bigint(opts) | BIGINT column | nullable |
| zdb.float(opts) | FLOAT column | nullable |
| zdb.decimal(opts) | DECIMAL column | precision, scale, nullable |
| zdb.boolean(opts) | BOOLEAN column | default, nullable |
| zdb.date(opts) | DATE column | nullable |
| zdb.datetime(opts) | DATETIME column | nullable |
| zdb.timestamp(opts) | TIMESTAMP column | auto: 'create'\|'update', nullable |
| zdb.json(opts) | JSON column | nullable |
| zdb.array(itemSchema, opts) | ARRAY column (stored as JSON) | nullable |
| zdb.enum(values, opts) | ENUM column | default, nullable |
| zdb.foreignKey(table, opts) | Foreign key (bigint) | referenceColumn, nullable |
| zdb.foreignUuid(table, opts) | Foreign key (uuid) | referenceColumn, nullable |
Model Definition
const User = defineModel({
name: 'User', // Model name
table: 'users', // Database table
schema: UserSchema, // Zod schema
primaryKey: 'id', // Primary key column (default: 'id')
relations: {
// belongsTo: this model has foreign key
company: {
type: 'belongsTo',
model: () => Company,
foreignKey: 'company_id',
},
// hasMany: related model has foreign key
posts: {
type: 'hasMany',
model: () => Post,
foreignKey: 'user_id',
},
// hasOne: like hasMany but returns single record
profile: {
type: 'hasOne',
model: () => Profile,
foreignKey: 'user_id',
},
},
scopes: {
softDelete: true, // Use deleted_at column
timestamps: true, // Auto-manage created_at/updated_at
tenant: 'tenant_id', // Multi-tenant column (optional)
},
});Auto-Loading Models
Models are automatically loaded from the models/ directory when you create a database instance:
// models/User.js
const { defineModel, zdb } = require('webspresso');
module.exports = defineModel({
name: 'User',
table: 'users',
schema: zdb.schema({
id: zdb.id(),
email: zdb.string({ unique: true }),
name: zdb.string(),
created_at: zdb.timestamp({ auto: 'create' }),
updated_at: zdb.timestamp({ auto: 'update' }),
}),
});
// In your application code
const db = createDatabase({
client: 'pg',
connection: process.env.DATABASE_URL,
models: './models', // Optional, defaults to './models'
});
// Models are automatically loaded, use getRepository with model name
const UserRepo = db.getRepository('User');Model File Structure:
- Place model files in
models/directory (or custom path viaconfig.models) - Each file should export a model defined with
defineModel() - Files starting with
_are ignored (useful for shared utilities) - Models are loaded in alphabetical order
Repository API
const db = createDatabase({ client: 'pg', connection: '...' });
const UserRepo = db.getRepository('User'); // Use model name string
// Find by ID (with eager loading)
const user = await UserRepo.findById(1, { with: ['company', 'posts'] });
// Find one by conditions
const admin = await UserRepo.findOne({ email: '[email protected]' });
// Find all
const users = await UserRepo.findAll({ with: ['company'] });
// Create
const newUser = await UserRepo.create({
email: '[email protected]',
name: 'New User',
});
// Create many
const users = await UserRepo.createMany([
{ email: '[email protected]', name: 'User 1' },
{ email: '[email protected]', name: 'User 2' },
]);
// Update
const updated = await UserRepo.update(1, { name: 'Updated Name' });
// Update where
await UserRepo.updateWhere({ status: 'inactive' }, { status: 'banned' });
// Delete (soft delete if enabled)
await UserRepo.delete(1);
// Force delete (permanent)
await UserRepo.forceDelete(1);
// Restore soft-deleted
await UserRepo.restore(1);
// Count
const count = await UserRepo.count({ status: 'active' });
// Exists
const exists = await UserRepo.exists({ email: '[email protected]' });Query Builder
const users = await UserRepo.query()
.where({ status: 'active' })
.where('created_at', '>', '2024-01-01')
.whereIn('role', ['admin', 'moderator'])
.whereNotNull('email_verified_at')
.orderBy('name', 'asc')
.orderBy('created_at', 'desc')
.limit(10)
.offset(20)
.with('company', 'posts')
.list();
// First result
const user = await UserRepo.query()
.where({ email: '[email protected]' })
.first();
// Count
const count = await UserRepo.query()
.where({ status: 'active' })
.count();
// Pagination
const result = await UserRepo.query()
.where({ status: 'active' })
.orderBy('created_at', 'desc')
.paginate(1, 20); // page 1, 20 per page
// result = { data: [...], total: 150, page: 1, perPage: 20, totalPages: 8 }
// Soft delete scopes
await UserRepo.query().withTrashed().list(); // Include deleted
await UserRepo.query().onlyTrashed().list(); // Only deleted
// Multi-tenant
await UserRepo.query().forTenant(tenantId).list();Transactions
await db.transaction(async (trx) => {
const userRepo = trx.getRepository('User'); // Use model name
const postRepo = trx.getRepository('Post');
const user = await userRepo.create({ email: '[email protected]', name: 'New' });
await postRepo.create({ title: 'First Post', user_id: user.id });
// All changes committed on success
// Rolled back on error
});Migrations
CLI Commands:
# Run pending migrations
webspresso db:migrate
# Rollback last batch
webspresso db:rollback
# Rollback all
webspresso db:rollback --all
# Show migration status
webspresso db:status
# Create empty migration
webspresso db:make create_posts_table
# Create migration from model (scaffolding)
webspresso db:make create_users_table --model User
# Admin Panel Setup
webspresso admin:setup # Create admin_users migrationDatabase Config File (webspresso.db.js):
module.exports = {
client: 'pg', // or 'mysql2', 'better-sqlite3'
connection: process.env.DATABASE_URL,
migrations: {
directory: './migrations',
tableName: 'knex_migrations',
},
// Environment overrides
production: {
connection: process.env.DATABASE_URL,
pool: { min: 2, max: 10 },
},
};Programmatic API:
const db = createDatabase({
client: 'pg',
connection: process.env.DATABASE_URL,
migrations: { directory: './migrations' },
});
await db.migrate.latest(); // Run pending
await db.migrate.rollback(); // Rollback last batch
await db.migrate.rollback({ all: true }); // Rollback all
const status = await db.migrate.status(); // Get statusMigration Scaffolding
Generate migration from model schema:
const { scaffoldMigration } = require('webspresso');
const migration = scaffoldMigration(User);
// Outputs complete migration file content with:
// - All columns with proper types
// - Indexes
// - Foreign key constraints
// - Up and down functionsSupported Databases
Install the appropriate driver as a peer dependency:
# PostgreSQL
npm install pg
# MySQL
npm install mysql2
# SQLite
npm install better-sqlite3Design Philosophy
| Boundary | Zod's Job | ORM's Job |
|----------|-----------|-----------|
| Schema definition | Type shape, validation rules | Column metadata extraction |
| Input validation | .parse() / .safeParse() | Never - pass through to Zod |
| Query building | N/A | Full ownership |
| Relation resolution | N/A | Eager loading with batch queries |
| Timestamps/SoftDelete | N/A | Auto-inject on operations |
N+1 Prevention: Relations are always loaded with batch WHERE IN (...) queries, never with individual queries per record.
Database Seeding
CLI Command:
The easiest way to seed your database is using the CLI command:
# Run seeds (requires seeds/index.js)
webspresso seed
# Setup seed files if they don't exist
webspresso seed --setup
# Use custom database config
webspresso seed --config ./custom-db-config.js
# Use different environment
webspresso seed --env productionThe webspresso seed command:
- Automatically loads all models from
models/directory - Generates fake data based on model schemas
- Creates 10 records per model by default
- Uses smart field detection for appropriate fake data
Manual Setup:
Generate fake data for testing and development using @faker-js/faker:
npm install @faker-js/fakerBasic Usage:
const { faker } = require('@faker-js/faker');
const db = createDatabase({ /* config */ });
const seeder = db.seeder(faker);
// Generate a single record
const user = await seeder.factory('User').create();
// Generate multiple records
const users = await seeder.factory('User').create(10);
// Generate without saving (for testing)
const userData = seeder.factory('User').make();Define Factories with Defaults and States:
seeder.defineFactory('User', {
// Default values
defaults: {
status: 'pending',
},
// Custom generators
generators: {
username: (f) => f.internet.username().toLowerCase(),
},
// Named states for variations
states: {
admin: { role: 'admin', status: 'active' },
verified: (f) => ({
status: 'verified',
verified_at: f.date.past().toISOString(),
}),
},
});
// Use states
const admin = await seeder.factory('User').state('admin').create();
const verified = await seeder.factory('User').state('verified').create();Smart Field Detection:
The seeder automatically generates appropriate fake data based on column names:
| Field Name Pattern | Generated Data |
|-------------------|----------------|
| email, *_email | Valid email address |
| name, first_name, last_name | Person names |
| username | Username |
| title | Short sentence |
| content, body, description | Paragraphs |
| slug | URL-safe slug |
| phone, tel | Phone number |
| address, city, country | Location data |
| price, amount, cost | Decimal numbers |
| *_url, avatar, image | URLs |
Override and Custom Generators:
const user = await seeder.factory('User')
.override({ email: '[email protected]' })
.generators({
code: (f) => `USR-${f.string.alphanumeric(8)}`,
})
.create();Batch Seeding:
// Seed multiple models at once
const results = await seeder.run([
{ model: 'Company', count: 5 },
{ model: 'User', count: 20, state: 'active' },
{ model: 'Post', count: 50 },
]);
// Access results
console.log(results.Company); // Array of 5 companies
console.log(results.User); // Array of 20 usersCleanup:
// Truncate specific tables
await seeder.truncate('User');
await seeder.truncate(['User', 'Post']);
// Clear all registered model tables
await seeder.clearAll();Schema Explorer Plugin
A plugin that exposes ORM schema information via API endpoints. Useful for frontend code generation, documentation, or admin tools.
Setup:
const { createApp, schemaExplorerPlugin } = require('webspresso');
const app = createApp({
plugins: [
schemaExplorerPlugin({
path: '/_schema', // Endpoint path (default: '/_schema')
enabled: true, // Force enable (default: auto based on NODE_ENV)
exclude: ['Secret'], // Exclude specific models
includeColumns: true, // Include column metadata
includeRelations: true, // Include relation metadata
includeScopes: true, // Include scope configuration
authorize: (req) => { // Custom authorization
return req.headers['x-api-key'] === 'secret';
},
}),
],
});Endpoints:
GET /_schema- List all modelsGET /_schema/:modelName- Get single model detailsGET /_schema/openapi- Export in OpenAPI 3.0 schema format
Example Response (GET /_schema):
{
"meta": {
"version": "1.0.0",
"generatedAt": "2024-01-01T12:00:00.000Z",
"modelCount": 2
},
"models": [
{
"name": "User",
"table": "users",
"primaryKey": "id",
"columns": [
{ "name": "id", "type": "bigint", "primary": true, "autoIncrement": true },
{ "name": "email", "type": "string", "unique": true },
{ "name": "company_id", "type": "bigint", "references": "companies" }
],
"relations": [
{ "name": "company", "type": "belongsTo", "relatedModel": "Company", "foreignKey": "company_id" }
],
"scopes": { "softDelete": true, "timestamps": true, "tenant": null }
}
]
}Plugin API (programmatic usage):
const plugin = schemaExplorerPlugin();
// Plugin API can be used by other plugins or in code
const models = plugin.api.getModels(); // All models
const user = plugin.api.getModel('User'); // Single model
const names = plugin.api.getModelNames(); // Model namesDevelopment
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageLicense
MIT
