Skip to content

wpress-x

A tiny, fast, batteries-included web framework for Node.js. The Express API you already know — with zero dependencies.

v2.0.0 0 dependencies Node 18+ MIT 88 tests

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)
History note. The project was previously called 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.json has no dependencies section — 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/. No node_modules archaeology.

Why wpress-x

Everything in the right-hand column is already inside wpress-x:

CapabilityExpresswpress-x
Packages installed with it~500
body-parserinstall itbuilt in
cookie-parserinstall itbuilt in
express-sessioninstall itbuilt in
multer (uploads)install itbuilt in
morgan (logging)install itbuilt in
helmetinstall itbuilt in
corsinstall itbuilt in
compressioninstall itbuilt in
csurf (deprecated upstream)install itbuilt in
serve-static, serve-index, serve-faviconinstall thembuilt in
express-rate-limitinstall itbuilt in
method-override, response-timeinstall thembuilt in
EJS / Pug / Handlebars / Markdowninstall thembuilt in
TypeScript types@types/expressbuilt 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

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 })
One mount per prefix (v2.0.0). The first mount at a path claims that path entirely — a second 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))
Inside a handler, 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.

PropertyTypeDescription
req.paramsobjectRoute parameters, e.g. { id: '42' }
req.queryobjectParsed query string; nested by default — ?b[c]=2 becomes { b: { c: '2' } }
req.bodyanyParsed by WpressX.json() / urlencoded() / text() / raw()
req.filesobjectUploads parsed by WpressX.multipart()
req.rawBodyBufferThe untouched body buffer, when the parser ran with verify
req.cookiesobjectPlain cookies (needs WpressX.cookieParser())
req.signedCookiesobjectCookies verified against the secret
req.sessionSessionSession object (needs WpressX.session())
req.pathstringPath portion of the URL
req.hostname / req.hoststringHost header, port excluded / included
req.ip / req.ipsstring / arrayClient address, honouring trust proxy
req.protocolstringhttp or https, proxy-aware
req.securebooleanTrue when the protocol is https
req.xhrbooleanTrue when X-Requested-With: XMLHttpRequest
req.fresh / req.stalebooleanCache state, based on If-None-Match
req.subdomainsarraySubdomains, offset by subdomain offset
req.methodstringThe HTTP verb
req.originalUrlstringFull original URL, untouched by mounting
req.baseUrlstringThe 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()
Behind a load balancer. Set 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

OptionDefaultDescription
maxAge0Cache-Control max-age; accepts '1h' strings
index'index.html'Directory index file, or false
extensionsfalseExtension fallbacks, e.g. ['html']
dotfiles'ignore''allow' · 'deny' · 'ignore'
etag / lastModifiedtrueValidator headers
acceptRangestrueByte-range support
cacheControltrueSend Cache-Control at all
immutablefalseAdds immutable to Cache-Control
redirecttrueRedirect /dir to /dir/
fallthroughtrueCall 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.

You can also pass the views directory straight to the factory: 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('/')))
OptionDefaultDescription
secretnullSigning secret (string, or an array for rotation)
name'wpress-x.sid'Session cookie name
storeMemoryWpressX.session.MemoryStore or .FileStore
cookie{}maxAge, httpOnly, secure, sameSite, path, domain
resavefalseSave on every request, even if unmodified
saveUninitializedfalsePersist brand-new empty sessions
rollingfalseReset expiry on every response
unset'keep''keep' or 'destroy' when a key is deleted
genidbuilt-in(req) => string id generator
ttlServer-side expiry for stored sessions
sweepIntervalHow often expired sessions are reaped

Session objects expose regenerate(), destroy(), save(), touch() and reload().

The default 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.

MiddlewareWhat 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
Body parsers have a hard 100 kb default limit and answer 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):

Scenarioreq/smean msp50p95p99vs raw
raw http.Server8,0842.472.215.026.54100%
wpress-x (1 route)5,4303.683.255.898.7467%
wpress-x + 6 middleware4,1894.774.267.1711.0652%
wpress-x (router + params)5,8843.403.134.726.9273%

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

MethodDescription
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.localsValues 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: morgan WpressX.logger, compression WpressX.compress, and so on.
  • Template engines. Remove ejs, pug and handlebars from package.json, or keep them via app.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.
A good order of attack: delete the middleware packages from package.json, run the app, fix the require lines the error messages point at, then run your test suite.