wpress-x
A tiny, fast, batteries-included web framework for Node.js. The Express API you already know — with zero dependencies.
Introduction
wpress-x is a web framework for Node.js that keeps the Express API
intact while shipping everything you would normally install separately: body
parsing, cookies, sessions, file uploads, logging, security headers, CORS,
compression, rate limiting, CSRF, static files, directory listings, and four
template engines (EJS, Pug, Handlebars, Markdown).
The whole framework is one require away and about 13,000 lines under lib/:
const WpressX = require('wpress-x')
const app = WpressX()
app.get('/hello/:name', (req, res) => {
res.json({ hello: req.params.name })
})
app.done(3000)
press and
wpress. Every old spelling still works:
WpressX.Press, WpressX.Wpress, and app.us()
(the original spelling of app.use()) are all kept as aliases.
Design goals
- Zero dependencies.
package.jsonhas nodependenciessection — and never will. - Express-compatible. Most Express apps run unchanged, and stock Express middleware works too.
- Batteries included. Sixteen middleware modules and five view engines ship in the box.
- Fast enough to matter. Roughly double Express's throughput (see benchmarks).
- Readable source. Everything lives in
lib/. Nonode_modulesarchaeology.
Why wpress-x
Everything in the right-hand column is already inside wpress-x:
| Capability | Express | wpress-x |
|---|---|---|
| Packages installed with it | ~50 | 0 |
body-parser | install it | built in |
cookie-parser | install it | built in |
express-session | install it | built in |
multer (uploads) | install it | built in |
morgan (logging) | install it | built in |
helmet | install it | built in |
cors | install it | built in |
compression | install it | built in |
csurf (deprecated upstream) | install it | built in |
serve-static, serve-index, serve-favicon | install them | built in |
express-rate-limit | install it | built in |
method-override, response-time | install them | built in |
| EJS / Pug / Handlebars / Markdown | install them | built in |
| TypeScript types | @types/express | built in |
Throughput vs a bare http.Server | ~35–45% | ~70% |
No supply chain
No transitive packages, no version drift between 50 modules that were never meant to agree.
Drop-in familiar
(req, res, next), routers, param callbacks, error middleware — exactly where you expect them.
Five view engines
EJS, Pug, Handlebars, Markdown and plain templates — no build step, no extra install.
Secure defaults
Helmet headers, CSRF tokens, signed cookies and rate limiting are one app.use away.
Installation
Requires Node.js 18 or newer.
npm i wpress-x
yarn add wpress-x
pnpm add wpress-x
bun add wpress-x
Or scaffold a complete project with the bundled CLI:
npx wpress-x create my-app
cd my-app
npm start
Quick start
The smallest useful app: a JSON endpoint, a route parameter, body parsing and a static directory.
const WpressX = require('wpress-x')
const app = WpressX()
// middleware: logging + body parsing + static files
app.use(WpressX.logger('dev'))
app.use(WpressX.json())
app.use(WpressX.urlencoded({ extended: true }))
app.use(WpressX.static('public'))
// routes
app.get('/', (req, res) => res.send('<h1>Hello wpress-x</h1>'))
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, q: req.query })
})
app.post('/login', (req, res) => {
res.status(201).json({ user: req.body.name })
})
// start: app.done(), app.listen() and app.start() are the same call
app.done(3000, () => console.log('listening on http://localhost:3000'))
node app.js
curl http://localhost:3000/users/42?tab=posts
# {"id":"42","q":{"tab":"posts"}}
curl -X POST http://localhost:3000/login \
-H 'content-type: application/json' \
-d '{"name":"ali"}'
# {"user":"ali"}
app.done() prints a readable message instead of an
EADDRINUSE stack when the port is already taken — handy on shared
dev machines.
Your first app
A small but complete REST API. Note that nothing beyond wpress-x is
required — no cors package, no helmet, no
morgan.
'use strict'
const WpressX = require('wpress-x')
const app = WpressX()
/* ---------- middleware ---------- */
app.use(WpressX.helmet())
app.use(WpressX.cors({ origin: true, credentials: true }))
app.use(WpressX.compress())
app.use(WpressX.logger('dev'))
app.use(WpressX.json({ limit: '1mb' }))
app.use(WpressX.rateLimit({ windowMs: 60_000, max: 100 }))
/* ---------- in-memory data ---------- */
let nextId = 3
const todos = [
{ id: 1, title: 'Read the wpress-x docs', done: true },
{ id: 2, title: 'Ship something', done: false }
]
/* ---------- routes ---------- */
app.get('/api/todos', (req, res) => {
res.json({ data: todos, count: todos.length })
})
app.get('/api/todos/:id', (req, res, next) => {
const todo = todos.find((t) => t.id === Number(req.params.id))
if (!todo) return next(WpressX.createError(404, 'No such todo'))
res.json(todo)
})
app.post('/api/todos', (req, res) => {
const todo = { id: nextId++, title: req.body.title, done: false }
todos.push(todo)
res.status(201).json(todo)
})
app.patch('/api/todos/:id', (req, res, next) => {
const todo = todos.find((t) => t.id === Number(req.params.id))
if (!todo) return next(WpressX.createError(404, 'No such todo'))
Object.assign(todo, req.body)
res.json(todo)
})
app.delete('/api/todos/:id', (req, res) => {
const i = todos.findIndex((t) => t.id === Number(req.params.id))
if (i === -1) return res.sendStatus(404)
todos.splice(i, 1)
res.sendStatus(204)
})
/* ---------- errors (always last) ---------- */
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message })
})
app.done(3000, () => console.log(app.summary()))
app.summary() prints the route table it just registered, which makes
startup logs genuinely useful:
GET /api/todos
GET /api/todos/:id
POST /api/todos
PATCH /api/todos/:id
DELETE /api/todos/:id
Routing
Route paths support named parameters, regex constraints, optional segments, wildcards, RegExp objects and arrays of paths.
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }))
app.get('/num/:n(\\d+)', (req, res) => res.send(req.params.n)) // digits only
app.get('/opt/:id?', (req, res) => res.json({ id: req.params.id }))
app.get('/files/:path*', (req, res) => res.send(req.params.path))
app.get('/all/*', (req, res) => res.send('wildcard'))
app.get(/^\/regex\/(\d+)$/, (req, res) => res.send(req.params[0]))
app.get(['/a', '/b'], (req, res) => res.send('a or b'))
HTTP methods
app.get, app.post, app.put,
app.patch, app.delete, app.head,
app.options and app.all are defined for every entry in
http.METHODS. app.del is an alias of
app.delete.
Like Express, app.get(name) with a single argument reads a
setting; app.get('/path', fn) registers a route.
Chaining verbs on one path
app.route('/article/:id')
.get((req, res) => res.json({ id: req.params.id }))
.put((req, res) => res.send('updated'))
.delete((req, res) => res.send('deleted'))
Routers, groups and sub-apps
// a standalone router, mounted anywhere
const api = WpressX.Router()
api.use((req, res, next) => { res.setHeader('X-Api', 'wpress-x'); next() })
api.get('/users', (req, res) => res.json(users))
app.use('/api', api)
// a prefixed group (a router is created for you)
app.group('/v1', (router) => {
router.get('/ping', (req, res) => res.json({ pong: Date.now() }))
})
// a whole sub-application
const admin = WpressX()
admin.get('/', (req, res) => res.send('admin'))
app.use('/admin', admin)
// router options
const strict = WpressX.Router({ caseSensitive: true, strict: true, mergeParams: true })
app.use('/api', …) is never
reached, and requests that the first router does not match end in
404 rather than falling through. Put every route for a prefix on
one router, and use distinct prefixes otherwise. Verified against 2.0.0.
Param callbacks
app.param() runs once per matched value, which is the natural place
to load a record:
app.param('id', (req, res, next, value) => {
req.user = findUser(value)
if (!req.user) return next(WpressX.createError(404, 'No such user'))
next()
})
app.get('/users/:id', (req, res) => res.json(req.user))
next('route') skips the rest of the current
route and next('router') escapes the current router entirely.
Both behave exactly as they do in Express.
Middleware
A middleware is just (req, res, next). Register it globally with
app.use(), or scope it to a path prefix.
app.use((req, res, next) => { console.log(req.method, req.path); next() })
app.use('/admin', requireAuth) // path-scoped
app.use('/api', apiRouter) // mount a Router
app.use('/blog', blogApp) // mount a sub-App
app.use(WpressX.json(), WpressX.urlencoded()) // several at once
app.us(middleware) // original spelling of use()
Error middleware
Error handlers take four arguments and are registered last. Thrown exceptions — synchronous or from a rejected promise — all land there.
app.use((err, req, res, next) => {
console.error(err)
res.status(err.status || 500).json({ error: err.message })
})
// or the built-in handler, which shows stacks outside production
app.use(WpressX.errorHandler({ showStack: process.env.NODE_ENV !== 'production' }))
Request req
Every handler receives a request object enhanced with parsed bodies, cookies, query strings, content negotiation and proxy-aware address info.
| Property | Type | Description |
|---|---|---|
req.params | object | Route parameters, e.g. { id: '42' } |
req.query | object | Parsed query string; nested by default — ?b[c]=2 becomes { b: { c: '2' } } |
req.body | any | Parsed by WpressX.json() / urlencoded() / text() / raw() |
req.files | object | Uploads parsed by WpressX.multipart() |
req.rawBody | Buffer | The untouched body buffer, when the parser ran with verify |
req.cookies | object | Plain cookies (needs WpressX.cookieParser()) |
req.signedCookies | object | Cookies verified against the secret |
req.session | Session | Session object (needs WpressX.session()) |
req.path | string | Path portion of the URL |
req.hostname / req.host | string | Host header, port excluded / included |
req.ip / req.ips | string / array | Client address, honouring trust proxy |
req.protocol | string | http or https, proxy-aware |
req.secure | boolean | True when the protocol is https |
req.xhr | boolean | True when X-Requested-With: XMLHttpRequest |
req.fresh / req.stale | boolean | Cache state, based on If-None-Match |
req.subdomains | array | Subdomains, offset by subdomain offset |
req.method | string | The HTTP verb |
req.originalUrl | string | Full original URL, untouched by mounting |
req.baseUrl | string | The path on which a router was mounted |
Methods
req.get('user-agent') // header lookup, case-insensitive
req.accepts(['json', 'html']) // 'json' | 'html' | false
req.acceptsLanguages(['en', 'ur']) // 'en' | 'ur' | false
req.acceptsCharsets(['utf-8'])
req.acceptsEncodings(['gzip'])
req.is('json') // 'json' | false | null
req.param('id', 'default') // params, then body, then query
req.range(size) // parsed Range header
req.cookie('name') // signed first, plain second
req.csrfToken() // with WpressX.csrf()
app.set('trust proxy', 1) (or true, a list,
'loopback', or a function) and req.ip /
req.protocol will report the client's real values instead of the
proxy's.
Response res
res.send('text') // text/html
res.send({ a: 1 }) // application/json
res.send(Buffer) // application/octet-stream
res.send(404) // treated as a status code
res.json(obj) res.jsonp(obj) res.text(str) res.html(str)
res.status(201).json(obj)
res.sendStatus(404) // 404 + "Not Found"
res.type('json') res.set('X-A', '1') res.get('X-A') res.append('Link', v)
res.vary('Accept') res.links({ next: '/page/2' })
res.redirect('/done') // 302
res.redirect(301, 'https://example.com')
res.cookie('n', 1, { signed: true, httpOnly: true, maxAge: 3600000 })
res.clearCookie('n')
res.attachment('report.pdf')
res.format({
'text/html': () => res.send('<p>hi</p>'),
'application/json': () => res.json({ hi: true }),
default: () => res.sendStatus(406)
})
res.render('index', { title: 'Hi' })
res.sse(data, 'event', id) // Server-Sent Events
res.sseEnd()
res.stream(readableStream) // pipe with automatic cleanup
res.send sets ETag, Content-Type and
Content-Length, handles HEAD, and answers conditional
GETs with 304. res.json respects the
json spaces, json replacer, json escape
and jsonp callback name settings.
Locals
res.locals inherits from app.locals through the
prototype chain, so app-wide values are visible in every template with no
copying:
app.locals.siteName = 'wpress-x' // available everywhere
app.use((req, res, next) => { res.locals.user = req.user; next() })
// templates can now read both `siteName` and `user`
Sending files
res.sendFile streams with Range support
(206), ETag, Last-Modified, conditional
GET (304) and HEAD handling. It refuses to escape its
root — a traversal attempt is answered with 403.
app.get('/cv', (req, res) => {
res.sendFile('cv.pdf',
{ root: __dirname + '/files', maxAge: '1d' },
(err) => { if (err) res.status(err.status || 500).send('no file') }
)
})
// force a download with a custom filename
app.get('/dl', (req, res) => res.download('/tmp/report.pdf', 'report.pdf'))
// serve a whole directory, with caching and extension fallbacks
app.use(WpressX.static(__dirname + '/public', { maxAge: '1h', extensions: ['html'] }))
// and let people browse it
app.use('/files', WpressX.serveIndex(__dirname + '/public'))
WpressX.static options
| Option | Default | Description |
|---|---|---|
maxAge | 0 | Cache-Control max-age; accepts '1h' strings |
index | 'index.html' | Directory index file, or false |
extensions | false | Extension fallbacks, e.g. ['html'] |
dotfiles | 'ignore' | 'allow' · 'deny' · 'ignore' |
etag / lastModified | true | Validator headers |
acceptRanges | true | Byte-range support |
cacheControl | true | Send Cache-Control at all |
immutable | false | Adds immutable to Cache-Control |
redirect | true | Redirect /dir to /dir/ |
fallthrough | true | Call next() instead of 404 |
setHeaders | — | (res, path, stat) => void hook |
Uploads
// keep uploads in memory (req.files.field.buffer)
app.use(WpressX.multipart({ multiple: true }))
// or write them straight to disk
app.use(WpressX.upload({ dest: 'uploads', limits: { fileSize: 5 * 1024 * 1024 } }))
app.post('/avatar', (req, res) => {
const file = req.files.avatar
res.json({ name: file.originalname, size: file.size, path: file.path })
})
Options: dest (write to a directory), limits,
multiple, preserveExtension. Without dest,
files arrive as Buffers in memory.
View engines
Five engines ship with wpress-x — no npm install ejs, no Pug build
step. Configure the directory and the default extension, then render.
app.set('views', __dirname + '/views')
app.set('view engine', 'ejs') // the default for res.render
app.get('/', (req, res) => res.render('index', { title: 'Hello' }))
app.get('/about', (req, res) => res.render('about.md')) // any extension
app.get('/p', (req, res) => res.render('page.pug'))
app.get('/h', (req, res) => res.render('page.hbs'))
Anything you pass to res.render becomes a local, and so do
app.locals and res.locals. wpress-x also
auto-detects: res.render('about') finds
about.md even when the default engine is EJS.
WpressX({ views: './views', viewEngine: 'ejs' }).
EJS .ejs
<h1><%= title %></h1> <!-- escaped -->
<%- include('partials/nav', { title }) %>
<%# a comment %>
<%% a literal <% tag %>
<% items.forEach(function (item) { -%>
<li><%= item.name %></li>
<% }) %>
<% %> scriptlets, <%= %> escaped,
<%- %> raw, <%# %> comments,
<%% / %%> literals, -%> newline
trimming, plus include() and layout(). Missing
variables render as empty instead of throwing.
Pug .pug
extends layout
block content
h1.heading#top= title
ul
each item, i in items
li= i + ': ' + item
mixin badge(label)
span.badge= label
+badge('new')
p inside the mixin's block
Indentation syntax, void elements, doctypes, #id /
.class shorthand, attributes (comma or space separated),
= / != / #{} / !{},
each / while / for / if /
else if / else / unless, block and piped
text, // and //-, include,
extends with block / append /
prepend, and mixin / +call with blocks.
Handlebars .hbs
<h1>{{title}}</h1>
{{{rawHtml}}} {{&alsoRaw}}
{{#each items}}<li>{{@index}}: {{this.name}}</li>{{else}}<li>none</li>{{/each}}
{{#if ok}}yes{{else}}no{{/if}}
{{#with user}}{{name}} of {{../org}}{{/with}}
{{upper title}} {{join names ", "}}
{{> footer}}
Mustaches, triple-stash raw output, dotted paths, ../ parent
scopes, @index / @key / @first /
@last / @root, if /
unless / each / with with
{{else}}, helpers with positional and hash arguments, registered
partials, and file-based partials loaded from views/partials/.
Markdown .md
Headings, emphasis, links, images, autolinks, fenced and indented code blocks,
blockquotes, nested ordered/unordered lists, pipe tables, horizontal rules and
inline code. Output is HTML-escaped first, so a <script> in a
comment cannot run, and javascript: / data: URLs are
neutralised.
# About
Built with **wpress-x** — zero dependencies.
| Engine | Extension |
| ------ | --------- |
| EJS | .ejs |
| Pug | .pug |
Plain templates .html
Raw files with optional {{ name }} interpolation — handy for static
HTML pages that only need a couple of values.
Bring your own engine
app.engine('njk', (filePath, options, callback) => {
callback(null, myRenderer(filePath, options))
})
// any Express-compatible engine drops straight in
app.engine('ejs', require('ejs').renderFile)
Cookies & sessions
app.use(WpressX.cookieParser(process.env.SECRET))
app.use(WpressX.session({
secret: process.env.SECRET,
name: 'wpress-x.sid',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 3600000, httpOnly: true, sameSite: 'lax' }
}))
app.get('/hits', (req, res) => {
req.session.hits = (req.session.hits || 0) + 1
res.json({ hits: req.session.hits })
})
app.post('/logout', (req, res) => req.session.destroy(() => res.redirect('/')))
| Option | Default | Description |
|---|---|---|
secret | null | Signing secret (string, or an array for rotation) |
name | 'wpress-x.sid' | Session cookie name |
store | Memory | WpressX.session.MemoryStore or .FileStore |
cookie | {} | maxAge, httpOnly, secure, sameSite, path, domain |
resave | false | Save on every request, even if unmodified |
saveUninitialized | false | Persist brand-new empty sessions |
rolling | false | Reset expiry on every response |
unset | 'keep' | 'keep' or 'destroy' when a key is deleted |
genid | built-in | (req) => string id generator |
ttl | — | Server-side expiry for stored sessions |
sweepInterval | — | How often expired sessions are reaped |
Session objects expose regenerate(), destroy(), save(), touch() and reload().
MemoryStore is for development only — it does not scale
past one process. In production use
WpressX.session.FileStore({ dir: './.sessions', ttl: 86400000 })
or plug in your own store.
CSRF protection
app.use(WpressX.cookieParser('secret'))
app.use(WpressX.session({ secret: 'secret', resave: false, saveUninitialized: false }))
app.use(WpressX.csrf()) // needs a session (or cookie) to store the secret
app.get('/form', (req, res) => {
res.send(`<form method="post" action="/submit">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<button>Send</button>
</form>`)
})
Options: value (a custom token reader), cookie,
cookieName, sessionKey, ignoreMethods
(default ['GET', 'HEAD', 'OPTIONS']), saltLength and
secretLength. The current token is also exposed to templates as
res.locals.csrfToken.
Built-in middleware
Every one of these hangs off the factory: WpressX.json, WpressX.static, and so on.
| Middleware | What it does |
|---|---|
WpressX.json(opts) | JSON bodies. limit, strict, reviver, verify, gzip/deflate/br |
WpressX.urlencoded(opts) | Form bodies, simple or extended (nested, arrays, dots) |
WpressX.text(opts) / WpressX.raw(opts) | String and Buffer bodies |
WpressX.bodyParser(opts) | json + urlencoded in one call |
WpressX.multipart(opts) | multipart/form-data uploads; dest writes to disk |
WpressX.upload(opts) | Alias of multipart |
WpressX.cookieParser(secret) | req.cookies, req.signedCookies, req.cookie() |
WpressX.session(opts) | Sessions with Memory/File stores, regenerate, destroy |
WpressX.static(root, opts) | Static files with ranges, ETags, caching |
WpressX.serveIndex(root) | Directory listings |
WpressX.favicon(path?) | Serves /favicon.ico — an inline transparent icon by default |
WpressX.cors(opts) | CORS + preflight; string / array / RegExp / function origins |
WpressX.helmet(opts) | 13 security headers plus a CSP directive builder |
WpressX.compress(opts) | br / gzip / deflate with a size threshold |
WpressX.rateLimit(opts) | Rate limiting with RateLimit-* and Retry-After |
WpressX.csrf(opts) | Synchroniser tokens, req.csrfToken(), res.locals.csrfToken |
WpressX.methodOverride(getter) | _method from query, body or header |
WpressX.responseTime(opts) | Sets X-Response-Time |
WpressX.logger(format) | dev, tiny, short, common, combined, custom, :tokens |
WpressX.errorHandler(opts) | HTML/JSON error pages, stacks outside production |
A sensible production stack
app.use(WpressX.logger('dev'))
app.use(WpressX.helmet())
app.use(WpressX.cors({ origin: true, credentials: true }))
app.use(WpressX.compress({ threshold: 512 }))
app.use(WpressX.json({ limit: '1mb' }))
app.use(WpressX.urlencoded({ extended: true }))
app.use(WpressX.cookieParser(process.env.SECRET))
app.use(WpressX.session({ secret: process.env.SECRET, resave: false, saveUninitialized: false }))
app.use(WpressX.static('public'))
app.use(WpressX.rateLimit({ windowMs: 60_000, max: 100 }))
app.use(WpressX.errorHandler()) // always last
413 when it is exceeded. Raise it explicitly with
{ limit: '1mb' } — a globally unlimited parser is how servers get
squashed.
Application settings
app.set('views', 'views') app.get('views')
app.set('view engine', 'ejs')
app.set('view cache', true) // on by default in production
app.set('trust proxy', 1) // true | number | list | 'loopback' | fn
app.set('etag', 'weak') // 'weak' | 'strong' | false | fn
app.set('query parser', 'extended') // 'extended' | 'simple' | false | fn
app.set('json spaces', 2)
app.set('jsonp callback name', 'callback')
app.set('subdomain offset', 2)
app.set('x-powered-by', false) // hide the X-Powered-By header
app.enable('strict routing') // app.enable / app.disable / app.enabled
Introspection
app.routes()
// [ { methods: ['GET'], path: '/api/todos', handlers: 1 },
// { methods: ['GET'], path: '/api/todos/:id', handlers: 1 }, ... ]
console.log(app.summary())
// GET /api/todos
// GET /api/todos/:id
// POST /api/todos
Server lifecycle
const server = app.done(3000, () => console.log('up')) // returns http.Server
app.listen(3000) // alias
app.start(3000) // alias
app.address() // the bound address, once listening
app.close(() => console.log('bye'))
// HTTPS, or anything else createServer accepts
const httpsServer = app.createServer({ key, cert })
httpsServer.listen(443)
Error handling
Create errors with WpressX.createError() and handle them in one
place. Synchronous throws, next(err) and rejected promises all end
up in the same handler.
app.get('/boom', () => {
throw WpressX.createError(418, 'I am a teapot')
})
// createError() accepts all of these shapes
WpressX.createError(404)
WpressX.createError(404, 'Not found')
WpressX.createError(404, new Error('Not found'))
WpressX.createError(new Error('Not found'))
WpressX.createError(err, { headers: { 'X-Request-Id': id } })
// normalise anything that comes out of a callback
const err = WpressX.normalizeError(whatever)
// always last
app.use(WpressX.errorHandler())
Uncaught errors, next(err) and rejected promises all end up in the
same place: your error middleware, or WpressX.errorHandler() if you
have none.
Command line
The package ships a wpress-x binary. The scaffolding is inlined in
the CLI, so there is no template repository to clone.
wpress-x create my-app # scaffold a project
wpress-x routes # print the route table of ./app.js
wpress-x serve [file] # run an app file (default ./app.js)
wpress-x version
wpress-x create writes a working project — app.js, a views/ folder, a public/ folder and a package.json with start and dev scripts.
Benchmarks
npm run bench # 50k requests, 20 concurrent
node bench/bench.js --n 20000 -c 50
On the machine this was measured on (Node 20):
| Scenario | req/s | mean ms | p50 | p95 | p99 | vs raw |
|---|---|---|---|---|---|---|
raw http.Server | 8,084 | 2.47 | 2.21 | 5.02 | 6.54 | 100% |
| wpress-x (1 route) | 5,430 | 3.68 | 3.25 | 5.89 | 8.74 | 67% |
| wpress-x + 6 middleware | 4,189 | 4.77 | 4.26 | 7.17 | 11.06 | 52% |
| wpress-x (router + params) | 5,884 | 3.40 | 3.13 | 4.72 | 6.92 | 73% |
wpress-x keeps roughly 70% of a bare http.Server's
throughput on a single route and still ~52% with six middleware in the stack —
about double what Express manages, with none of the dependency weight. Run
npm run bench on your own hardware before quoting numbers.
API cheat sheet
Everything below is exported from the single require('wpress-x') import.
The factory
const WpressX = require('wpress-x')
const app = WpressX({ views: './views', viewEngine: 'ejs', env: 'production' })
WpressX.version // '2.0.0'
WpressX.Router(opts) // a new Router
WpressX.Application // the Application class
WpressX.request // the request prototype
WpressX.response // the response prototype
WpressX.METHODS // the supported HTTP verbs
WpressX.Press // aliases kept for older names
WpressX.Wpress
Application
| Method | Description |
|---|---|
app.set(k, v) / app.get(k) | Read or write a setting |
app.enable(k) / app.disable(k) | Boolean settings |
app.enabled(k) / app.disabled(k) | Query a boolean setting |
app.use(path?, ...fn) | Register middleware (app.us is an alias) |
app.METHOD(path, ...fn) | Route a verb: get, post, put, patch, delete, all, … |
app.route(path) | Chain several verbs on one path |
app.group(prefix, fn) | Bundle routes under a prefix |
app.param(name, fn) | Run a callback for a route parameter |
app.engine(ext, fn) | Register a view engine |
app.render(view, opts, cb) | Render a view without a request |
app.done(port, cb) | Start listening; returns the http.Server |
app.listen(port, cb) / app.start(port, cb) | Aliases of done |
app.createServer(opts) | Build a server without binding it (for HTTPS) |
app.close(cb) / app.address() | Shut down / inspect the bound address |
app.routes() / app.summary() | Route table as data, or as a printable string |
app.locals | Values shared with every res.locals |
Router
const router = WpressX.Router({ caseSensitive: false, mergeParams: false, strict: false })
router.use(fn)
router.get(path, ...fn)
router.route(path).get(fn).post(fn)
router.param(name, fn)
router.all(path, ...fn)
router.del(path, fn) // alias of router.delete
Utilities
WpressX.createError(status, message?) // build an HttpError
WpressX.normalizeError(anything) // coerce into an Error
WpressX.HttpError // the error class
WpressX.mime.lookup('.png') // 'image/png'
WpressX.qs.parse('a[b]=1') // { a: { b: '1' } }
WpressX.etag(buffer) // strong ETag
WpressX.pathToRegexp('/u/:id') // { regexp, keys }
WpressX.sign(value, secret) // signed-cookie helpers
WpressX.unsign(value, secret)
WpressX.utils // the whole utility bag
WpressX.middleware // every middleware module, untouched
WpressX.engines // every built-in view engine
TypeScript
Declarations ship with the package — there is no @types/wpress-x to
install. package.json points types at
types/index.d.ts.
import WpressX, { Request, Response, NextFunction, Router } from 'wpress-x'
const app = WpressX()
app.get('/users/:id', (req: Request, res: Response) => {
res.json({ id: req.params.id })
})
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ error: err.message })
})
const router: Router = WpressX.Router()
app.use('/api', router)
Migrating from Express
In most projects the change is one line plus a package removal:
- const express = require('express')
- const bodyParser = require('body-parser')
- const morgan = require('morgan')
- const helmet = require('helmet')
- const cors = require('cors')
+ const WpressX = require('wpress-x')
- const app = express()
+ const app = WpressX()
- app.use(bodyParser.json())
+ app.use(WpressX.json())
What to check
- Middleware names. Replace the installed package with the built-in:
morganWpressX.logger,compressionWpressX.compress, and so on. - Template engines. Remove
ejs,pugandhandlebarsfrompackage.json, or keep them viaapp.engine()if you need their full feature set. - Third-party middleware. Anything written for Express keeps working — the
(req, res, next)contract is the same. app.listen. Still there.app.done()is just the original name.
package.json, run the app, fix the require lines the
error messages point at, then run your test suite.