
Security News
Meet Socket at Black Hat and DEF CON 2025 in Las Vegas
Meet Socket at Black Hat & DEF CON 2025 for 1:1s, insider security talks at Allegiant Stadium, and a private dinner with top minds in software supply chain security.
A modern, unopinionated, and performant Node.js server framework built on AsyncLocalStorage for elegant API interfaces.
A modern, unopinionated, and performant Node.js server framework built on AsyncLocalStorage for elegant API interfaces.
npm i dx-server jchain
dx-server uses the URLPattern API for routing, which is natively supported in Node.js v23.8.0 and later.
For Node.js < 23.8.0, you need to install a polyfill:
npm install urlpattern-polyfill
Then import it before using dx-server:
// Add this at the top of your entry file
import 'urlpattern-polyfill'
// Then import dx-server
import dxServer from 'dx-server'
To check if your runtime supports URLPattern natively:
if (typeof URLPattern === 'undefined') {
console.log('URLPattern not supported, polyfill required')
}
import {Server} from 'node:http'
import chain from 'jchain'
import dxServer, {getReq, getRes, router, setHtml, setText} from 'dx-server'
new Server().on('request', (req, res) => chain(
dxServer(req, res),
async next => {
try {
// Access req/res from anywhere - no prop drilling!
getRes().setHeader('Cache-Control', 'no-cache')
console.log(getReq().method, getReq().url)
await next()
} catch (e) {
console.error(e)
setHtml('internal server error', {status: 500})
}
},
router.get({
'/'() {setHtml('hello world')},
'/health'() {setText('ok')}
}),
() => setHtml('not found', {status: 404}),
)()).listen(3000, () => console.log('server is listening at 3000'))
import {Server} from 'node:http'
import chain from 'jchain'
import dxServer, {router, setJson, getJson} from 'dx-server'
interface User {
id: number
name: string
}
new Server().on('request', (req, res) => chain(
dxServer(req, res),
router.post({
async '/api/users'() {
const body = await getJson<{name: string}>()
if (!body?.name) {
setJson({error: 'Name required'}, {status: 400})
return
}
const user: User = {id: 1, name: body.name}
setJson(user, {status: 201})
}
}),
() => setJson({error: 'Not found'}, {status: 404})
)()).listen(3000)
import {Server} from 'node:http'
import chain from 'jchain'
import dxServer, {chainStatic, setHtml} from 'dx-server'
import {resolve} from 'node:path'
new Server().on('request', (req, res) => chain(
dxServer(req, res),
chainStatic('/*', {
root: resolve(import.meta.dirname, 'public'),
}),
() => setHtml('not found', {status: 404}),
)()).listen(3000)
This example requires: npm install express morgan helmet cors
import {Server} from 'node:http'
import {promisify} from 'node:util'
import chain from 'jchain'
import dxServer, {
getReq, getRes,
getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
setHtml, setJson, setText, setEmpty, setBuffer, setRedirect, setNodeStream, setWebStream, setFile,
router, connectMiddlewares, chainStatic, makeDxContext
} from 'dx-server'
import {expressApp} from 'dx-server/express'
import express from 'express'
import morgan from 'morgan'
// it is best practice to create custom error class for non-system error
class ServerError extends Error {
name = 'ServerError'
constructor(message, status = 400, code = 'unknown') {
super(message)
this.status = status
this.code = code
}
}
const authContext = makeDxContext(async () => {
if (getReq().headers.authorization) return {id: 1, name: 'joe (private)'}
})
function requireAuth() {
if (!authContext.value) throw new ServerError('Unauthorized', 401, 'UNAUTHORIZED')
}
const serverChain = chain(
next => {
// this is the difference between express and dx-server
// req, res can be accessed from anywhere via context which uses NodeJS's AsyncLocalStorage under the hood
getRes().setHeader('Cache-Control', 'no-cache')
return next() // must return or await
},
async next => {// global error catching for all following middlewares
try {
await next()
} catch (e) {// only app error message should be shown to user
if (e instanceof ServerError) setHtml(`${e.message} (code: ${e.code})`, {status: e.status})
else {// report system error
console.error(e)
setHtml('internal server error (code: internal)', {status: 500})
}
}
},
connectMiddlewares(
morgan('common'),
// cors(),
),
await expressApp(app => {// any express feature can be used. This requires express installed, with for e.g., `yarn add express`
app.set('trust proxy', true)
if (process.env.NODE_ENV !== 'production') app.set('json spaces', 2)
app.use('/public', express.static('public'))
}),
authContext.chain(), // chain context will set the context value to authContext.value in every request
router.post('/api/*', async ({next}) => {// example of catching error for all /api/* routes
try {
await next()
} catch (e) {
if (e instanceof ServerError) setJson({// only app error message should be shown to user
error: e.message,
code: e.code,
}, {status: e.status})
else {// report system error
console.error(e)
setJson({
message: 'internal server error',
code: 'internal'
}, {status: 500})
}
}
}),
router.post({
'/api/sample-public-api'() { // sample POST router
setJson({name: 'joe'})
},
'/api/me'() { // sample private router
requireAuth()
setJson({name: authContext.value.name})
},
}),
router.get('/', () => setHtml('ok')), // router.method() accepts 2 formats
router.get('/health', () => setText('ok')),
() => { // not found router
throw new ServerError('Not found', 404, 'NOT_FOUND')
},
)
const tcpServer = new Server()
.on('request', async (req, res) => {
try {
await chain(
dxServer(req, res, {jsonBeautify: process.env.NODE_ENV !== 'production'}), // basic dx-server context
serverChain,
)()
} catch (e) {
console.error(e)
res.end()
}
})
await promisify(tcpServer.listen.bind(tcpServer))(3000)
console.log('server is listening at 3000')
dx-server uses Node.js AsyncLocalStorage to provide request/response context globally, eliminating prop drilling:
// Access request/response from anywhere
import {getReq, getRes} from 'dx-server'
function someDeepFunction() {
const req = getReq() // No need to pass req through multiple layers
const res = getRes()
res.setHeader('X-Custom', 'value')
}
Body parsing functions are asynchronous and cached per request:
import {getJson, getText, getBuffer, getUrlEncoded} from 'dx-server'
// Async usage (lazy-loaded and cached)
const json = await getJson()
const text = await getText()
// Sync usage (requires chaining)
chain(
getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
next => {
console.log(getJson.value) // Access synchronously
return next()
}
)
Create reusable context objects with makeDxContext
:
import {makeDxContext, getReq} from 'dx-server'
// Create auth context
const authContext = makeDxContext(async () => {
const token = getReq().headers.authorization
if (!token) return null
return await validateToken(token) // Your validation logic
})
// Use in middleware
chain(
authContext.chain(), // Initialize for all requests
next => {
if (!authContext.value) {
setJson({error: 'Unauthorized'}, {status: 401})
return
}
return next()
}
)
import dxServer, {
// Request/Response access
getReq, getRes,
// Request body parsers
getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery,
// Response setters
setHtml, setJson, setText, setEmpty, setBuffer, setRedirect,
setNodeStream, setWebStream, setFile,
// Utilities
router, connectMiddlewares, chainStatic, makeDxContext
} from 'dx-server'
// Express integration (requires express installed)
import {expressApp, expressRouter} from 'dx-server/express'
// Low-level helpers
import {
setBufferBodyDefaultOptions,
bufferFromReq, jsonFromReq, rawFromReq, textFromReq,
urlEncodedFromReq, queryFromReq,
} from 'dx-server/helpers'
getReq()
- Get the current request objectgetRes()
- Get the current response objectAll body parsers are async, lazy-loaded, and cached per request:
getJson(options?)
- Parse JSON body (requires Content-Type: application/json
)getText(options?)
- Parse text body (requires Content-Type: text/plain
)getBuffer(options?)
- Get raw buffergetRaw(options?)
- Get raw body (requires Content-Type: application/octet-stream
)getUrlEncoded(options?)
- Parse URL-encoded form (requires Content-Type: application/x-www-form-urlencoded
)getQuery(options?)
- Parse query string parametersOptions:
{
bodyLimit?: number // Max body size in bytes (default: 100KB)
urlEncodedParser?: (search: string) => any
queryParser?: (search: string) => any
}
setJson(data, {status?, headers?})
- Send JSON responsesetHtml(html, {status?, headers?})
- Send HTML responsesetText(text, {status?, headers?})
- Send plain textsetBuffer(buffer, {status?, headers?})
- Send buffersetFile(path, options?)
- Send filesetNodeStream(stream, {status?, headers?})
- Send Node.js streamsetWebStream(stream, {status?, headers?})
- Send Web streamsetRedirect(url, {status?, headers?})
- Redirect responsesetEmpty({status?, headers?})
- Send empty responsemakeDxContext(fn)
- Create a custom context object
const ctx = makeDxContext(() => computeValue())
// Access value
await ctx() // Lazy load
ctx.value // Sync access (after loading)
ctx.get(req) // Get for specific request
// Set value
ctx.value = newValue
ctx.set(req, newValue)
connectMiddlewares(...middlewares)
- Use Connect/Express middlewarechainStatic(pattern, options)
- Serve static files
chainStatic('/public/*', {
root: '/path/to/files',
getPathname(matched){return matched.pathname}, // take URLPattern matched object, epects to return the file path
// the returned file path must be run through decodeURIComponent before returning
dotfiles: 'deny',
disableEtag: false,
lastModified: true
})
dx-server uses URLPattern API for routing, which differs from Express patterns:
import {router} from 'dx-server'
// Single route
router.get('/users/:id', ({matched}) => {
const {id} = matched.pathname.groups
setJson({userId: id})
})
// Multiple routes
router.post({
'/api/users'() { /* create user */ },
'/api/users/:id'({matched}) { /* update user */ },
'/api/users/:id/posts'({matched}) { /* get user posts */ }
})
// All HTTP methods supported
router.get(pattern, handler)
router.post(pattern, handler)
router.put(pattern, handler)
router.delete(pattern, handler)
router.patch(pattern, handler)
router.head(pattern, handler)
router.options(pattern, handler)
router.all(pattern, handler) // Any method
// Custom method
router.method('CUSTOM', pattern, handler)
// With prefix option
router.get({
'/users': listUsers,
'/users/:id': getUser
}, {prefix: '/api'}) // Routes become /api/users, /api/users/:id
Pattern | URLPattern | Express |
---|---|---|
Wildcard | /api/* | /api/* or /api/(.*) |
Optional trailing slash | {/}? | /path/? |
Named params | /:id | /:id |
Optional params | /:id? | /:id? |
Important differences:
'/foo'
matches /foo
but NOT /foo/
'/foo/'
matches /foo/
but NOT /foo
'/foo{/}?'
to match bothdx-server seamlessly integrates with Express applications and middleware:
import {expressApp, expressRouter} from 'dx-server/express'
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
chain(
// Use entire Express app
await expressApp(app => {
app.set('trust proxy', true)
app.set('json spaces', 2)
app.use(helmet())
app.use('/static', express.static('public'))
}),
// Or use Express router
expressRouter(router => {
router.use(cors())
router.get('/legacy', (req, res) => {
res.json({message: 'Express route'})
})
})
)
Pure functions for custom implementations:
import {
setBufferBodyDefaultOptions,
bufferFromReq, jsonFromReq, rawFromReq,
textFromReq, urlEncodedFromReq, queryFromReq
} from 'dx-server/helpers'
// Set global defaults
setBufferBodyDefaultOptions({
bodyLimit: 10 * 1024 * 1024, // 10MB
queryParser(search){return myCustomParser(search)}
})
// Use directly with req/res (no context required)
const json = await jsonFromReq(req, {bodyLimit: 1024})
const query = queryFromReq(req)
Always set appropriate body size limits to prevent DoS attacks:
chain(
getJson.chain({bodyLimit: 1024 * 1024}), // 1MB limit
// or globally:
dxServer(req, res, {bodyLimit: 5 * 1024 * 1024}) // 5MB
)
Never expose internal errors to clients:
class AppError extends Error {
constructor(message, status = 400, code = 'ERROR') {
super(message)
this.status = status
this.code = code
}
}
chain(
async next => {
try {
await next()
} catch (error) {
if (error instanceof AppError) {
setJson({error: error.message, code: error.code}, {status: error.status})
} else {
console.error(error) // Log for debugging
setJson({error: 'Internal server error'}, {status: 500})
}
}
}
)
Always validate input data:
router.post('/api/users', async () => {
const data = await getJson()
// Validate
if (!data?.email || !isValidEmail(data.email)) {
throw new AppError('Invalid email', 400, 'INVALID_EMAIL')
}
// Process...
})
Use security middleware:
import helmet from 'helmet'
import cors from 'cors'
chain(
connectMiddlewares(
helmet(),
cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true
})
)
)
import busboy from 'busboy'
router.post('/upload', () => {
const req = getReq()
const bb = busboy({headers: req.headers, limits: {fileSize: 10 * 1024 * 1024}})
bb.on('file', (name, file, info) => {
// Handle file stream
})
req.pipe(bb)
})
import {WebSocketServer} from 'ws'
const wss = new WebSocketServer({noServer: true})
server.on('upgrade', (request, socket, head) => {
if (request.url === '/ws') {
wss.handleUpgrade(request, socket, head, ws => {
wss.emit('connection', ws, request)
})
}
})
import rateLimit from 'express-rate-limit'
chain(
connectMiddlewares(
rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
})
)
)
import {createReadStream} from 'fs'
setNodeStream(createReadStream('large-file.pdf'))
chainStatic
with proper cache headers for static assets// Express
app.get('/users/:id', (req, res) => {
const {id} = req.params
res.json({userId: id})
})
// dx-server
router.get('/users/:id', ({matched}) => {
const {id} = matched.pathname.groups
setJson({userId: id})
})
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © Sang Tran
FAQs
A modern, unopinionated, and performant Node.js server framework built on AsyncLocalStorage for elegant API interfaces.
The npm package dx-server receives a total of 33 weekly downloads. As such, dx-server popularity was classified as not popular.
We found that dx-server demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Meet Socket at Black Hat & DEF CON 2025 for 1:1s, insider security talks at Allegiant Stadium, and a private dinner with top minds in software supply chain security.
Security News
CAI is a new open source AI framework that automates penetration testing tasks like scanning and exploitation up to 3,600× faster than humans.
Security News
Deno 2.4 brings back bundling, improves dependency updates and telemetry, and makes the runtime more practical for real-world JavaScript projects.