wpress-x
v2.0.1
Published
wpress-x - a tiny, fast, batteries-included web framework for Node.js. Express-compatible API, zero dependencies, view engines, middleware, sendFile and more built in.
Downloads
286
Maintainers
Readme
wpress-x
The next version of Express.js. Same API you already know, everything you would normally install separately already in the box, zero dependencies.
const WpressX = require('wpress-x')
const app = WpressX()
app.get('/hello/:name', (req, res) => {
res.json({ hello: req.params.name })
})
app.done(3000)Why wpress-x
| | Express | wpress-x |
| --- | --- | --- |
| npm dependencies when you install it | ~50 packages | 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 | install it (deprecated) | 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 |
| res.sendFile (the send package) | bundled, but external | built in |
| TypeScript types | @types/express | built in |
| Throughput vs a bare http.Server | ~35–45% | ~70% (see npm run bench) |
Everything is one file away: lib/. No node_modules archaeology, no supply
chain, no version drift between 50 packages that were never meant to agree.
Install
# scaffold a new project
node bin/wpress-x.js create my-app && cd my-app && npm start
# or run the tour of every feature from a checkout
node examples/app.js # http://localhost:3000
# or copy the folder into your project and
const WpressX = require('./wpress-x')package.json has no dependencies section, and it never will.
The API you started with
Your original wpress-x class is intact — every one of these still works:
const WpressX = require('wpress-x')
const app = WpressX()
app.routes /* .GET / .POST tables */ // kept, see app.routes()
app.get('/path', handler) // route + settings getter
app.post('/path', handler)
app.us(middleware) // app.use(), as you typed it
res.send(body) // strings, objects, Buffers
app.done(port, callback) // start listeningapp.done() returns the http.Server; app.listen() and app.start() are
aliases. res.send now also handles objects (JSON), numbers, Buffers, null
and undefined, and sets ETag, Content-Type, Content-Length and HEAD
handling for you.
Contents
- Routing
- Middleware:
app.use - Request
- Response
- Sending files
- View engines
- Built-in middleware
- Application settings
- Errors
- Benchmarks
- File map
Routing
app.get('/users/:id', (req, res) => res.json({ id: req.params.id }))
app.get('/num/:n(\\d+)', ...) // regex-constrained param
app.get('/opt/:id?', ...) // optional param
app.get('/files/:path*', ...) // zero-or-more segments
app.get('/all/*', ...) // wildcard
app.get(/^\/regex\/(\d+)$/, ...) // a RegExp route
app.get(['/a', '/b'], ...) // several paths, one handlerAlso: app.post, app.put, app.patch, app.delete, app.head,
app.options, app.all, and app.METHOD for anything in http.METHODS.
// chain 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'))
// bundle routes under a prefix
app.group('/v1', (router) => {
router.get('/ping', (req, res) => res.json({ pong: Date.now() }))
})
// a 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 whole sub-application
const admin = WpressX()
admin.get('/', (req, res) => res.send('admin'))
app.use('/admin', admin)
// param callbacks run once per value
app.param('id', (req, res, next, value) => {
req.user = findUser(value)
if (!req.user) return next(WpressX.createError(404, 'No such user'))
next()
})next('route') skips the rest of the current route; next('router') escapes
the current router.
Middleware: app.use
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(wpress-x.json(), wpress-x.urlencoded()) // several at oncePath arguments accept strings (with :params), arrays, and RegExps.
Error middleware has four arguments and goes last:
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message })
})Request
req.params // { id: '42' } route parameters
req.query // ?a=1&b[c]=2 -> { a: '1', b: { c: '2' } }
req.body // parsed by wpress-x.json() & friends
req.files // parsed by wpress-x.multipart()
req.rawBody // the buffer, when the parser ran with verify
req.cookies // wpress-x.cookieParser()
req.signedCookies // verified against the secret
req.cookie('name') // signed first, plain second
req.session // wpress-x.session()
req.get('user-agent')
req.accepts(['json', 'html'])
req.acceptsLanguages(['en', 'ur'])
req.is('json') // null when unknown, false when it is not
req.ip, req.ips, req.hostname, req.protocol, req.secure, req.subdomains
req.path, req.originalUrl, req.baseUrl, req.method, req.xhr, req.fresh
req.param('id', 'default') // params, then body, then query
req.csrfToken() // wpress-x.csrf()trust proxy is fully supported (true, a number, a list, a function, or
'loopback'), so req.ip and req.protocol report the client's values
behind a load balancer.
Response
res.send('text') // text/html
res.send({ a: 1 }) // application/json
res.send(Buffer) // application/octet-stream
res.send(404) // 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') res.redirect(301, 'https://example.com')
res.cookie('n', 1, { signed: true, httpOnly: true }) res.clearCookie('n')
res.attachment('report.pdf')
res.format({ 'text/html': fn, 'application/json': fn, default: fn })
res.render('index', { title: 'Hi' })
res.sse(data, 'event', id) // Server-Sent Events
res.stream(readable) // pipe with cleanup
res.setTimeout / res.flushHeaders() / res.socket / res.headersSentres.json respects json spaces, json replacer, json escape and
jsonp callback name. res.send sets ETag and answers conditional GETs.
Sending files
// stream a file with ranges, ETag, Last-Modified and HEAD support
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')
})
})
// as a download
app.get('/dl', (req, res) => res.download('/tmp/report.pdf', 'report.pdf'))
// a whole directory
app.use(WpressX.static(__dirname + '/public', { maxAge: '1h', extensions: ['html'] }))
app.use('/files', WpressX.serveIndex(__dirname + '/public'))sendFile refuses to escape its root (403), honours the dotfiles policy,
serves directory index files, and speaks Range: (206) and If-None-Match
(304).
View engines
Four engines ship with wpress-x. No npm install ejs, no .pug build step.
app.set('views', __dirname + '/views')
app.set('view engine', 'ejs') // .ejs is 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.
wpress-x-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, include() and layout().
Missing variables render as empty instead of throwing.
wpress-x-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 blockIndentation syntax, void elements, doctypes, #id/.class shorthand,
attributes (comma or space separated), =/!=/#{}/!{},
each/while/for/if/else if/else/unless, block text, piped text,
// and //-, include, extends with block/append/prepend, and
mixin/+call with blocks.
wpress-x-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/.
wpress-x-md
Markdown with headings, emphasis, links, images, autolinks, fenced and
indented code blocks, blockquotes, ordered/unordered nested lists,
pipe tables, horizontal rules and inline code — HTML-escaped first, so
<script> in a comment cannot run, and javascript:/data: URLs are
neutralised.
wpress-x-plain
Raw files, with optional {{ name }} interpolation for HTML templates.
Your own engine
app.engine('njk', (path, options, callback) => {
callback(null, myRenderer(path, options))
})Or use any Express-compatible engine: app.engine('ejs', require('ejs').renderFile).
Built-in middleware
All of them are on the factory: WpressX.json, WpressX.static, …
| Middleware | What it does |
| --- | --- |
| WpressX.json(opts) | JSON bodies, limit, strict, 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 |
| WpressX.multipart(opts) | multipart/form-data uploads, dest to write 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?) | /favicon.ico (inline transparent icon by default) |
| WpressX.cors(opts) | CORS + preflight, string/array/RegExp/function origins |
| WpressX.helmet(opts) | 13 security headers + 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/header |
| WpressX.responseTime(opts) | X-Response-Time |
| WpressX.logger(format) | dev, tiny, short, common, combined, custom, :tokens |
| WpressX.errorHandler(opts) | HTML/JSON error pages, stacks outside production |
app.use(WpressX.logger('dev'))
app.use(WpressX.compress())
app.use(WpressX.json({ limit: '1mb' }))
app.use(WpressX.multipart({ dest: 'uploads' }))
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())Application settings
app.set('views', 'views') app.get('views')
app.set('view engine', 'ejs')
app.set('view cache', true) // in production it is on by default
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 X-Powered-By
app.enable('strict routing') // app.enable / app.disable / app.enabledapp.locals is shared with every res.locals through the prototype chain, so
app.locals.siteName = 'wpress-x' is visible in every template.
Nice-to-haves:
app.routes() // [{ path, methods, fullPath, handlers }, ...]
app.summary() // a printable route tableErrors
const WpressX = require('wpress-x')
app.get('/boom', () => { throw WpressX.createError(418, 'I am a teapot') })
// normalise anything that comes out of a callback
const err = WpressX.normalizeError(whatever)
// always last
app.use(WpressX.errorHandler())createError(status), createError(status, message),
createError(status, Error), createError(Error) and
createError(err, { headers }) all work. Uncaught errors, next(err) and
rejected promises all end up in the same place: your error middleware, or
WpressX.errorHandler() if you have none.
Benchmarks
npm run bench # 50k requests, 20 concurrent
node bench/bench.js --n 20000 -c 50On the machine this was written 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.538 100%
wpress-x (1 route) 5,430 3.68 3.251 5.891 8.736 67%
wpress-x + 6 middleware 4,189 4.77 4.262 7.168 11.059 52%
wpress-x (router + params) 5,884 3.396 3.132 4.719 6.924 73%wpress-x keeps ~70% of a bare http.Server's throughput on a single route and
still ~52% with six middleware in the stack — roughly double what Express
manages, with none of the dependency weight.
Tests
npm test # = node test/run.js
node test/run.js # same thing, any Node from 18 to 2488 tests over routing, middleware, request/response, file sending, all five
view engines, template-file encodings and the error handler. node:test and a
tiny test/run.js loader — no test framework to install.
Why not
node --test test/? That works up to Node 20 and breaks from Node 22 onwards: the argument is treated as a module path and you getError: Cannot find module .../test.test/run.jsloads the*.test.jsfiles itself, so it behaves identically on every version and on Windows (wherecmd.exewould not expand a glob for you anyway).
Windows & Node 22+ notes
wpress-x is written to be platform-agnostic, and the test suite proves it:
- Line endings: every engine normalises CRLF/CR and strips a UTF-8 BOM before parsing, so templates saved by Notepad render the same as ones committed from Linux.
- Paths: file paths go through
path.join/path.resolve(backslashes are fine), while URLs are always joined with/— so directory listings produce working links on Windows. - Ports:
app.done(3000)prints a readable message instead of anEADDRINUSEstack when the port is taken. - Spaces in paths: the whole suite passes from a directory with spaces in its name.
File map
wpress-x/
├── index.js # require('wpress-x')
├── bin/wpress-x.js # CLI: create / routes / serve
├── lib/
│ ├── wpress-x.js # the factory + every static
│ ├── application.js # Application: settings, engines, mounting, done()
│ ├── request.js # Request prototype
│ ├── response.js # Response prototype
│ ├── view.js # View class + engine resolution
│ ├── router/ # Layer, Route, Router (matching, params, dispatch)
│ ├── middleware/ # 16 middleware modules (see the table above)
│ ├── engines/ # ejs, pug, hbs, markdown, plain
│ ├── internal/ # finalhandler
│ └── utils/ # mime, qs, url, path-to-regexp, etag, fresh,
│ # range, content-disposition, accepts, http-errors,
│ # send (sendFile), utils, scope
├── examples/ # a tour of every feature (node examples/app.js)
├── bench/ # dependency-free load generator
├── test/ # 88 tests + run.js loader
└── types/index.d.ts # TypeScript declarationsLicence
MIT.
