A modern, unopinionated, and performant Node.js server framework built on AsyncLocalStorage for elegant API interfaces.
- 🎯 Elegant API interface - No need to pass req/res objects through middleware chains
- 🔗 Chainable middleware - Elegant middleware composition with jchain
- 🚀 Context-based architecture - Access request/response from anywhere using AsyncLocalStorage
- 🔄 Express compatible - Use existing Express middleware and applications
- 📦 Zero dependencies - No runtime dependencies, all functionality built-in
- 🛡️ Built-in body parsing - JSON, text, URL-encoded, and raw body parsing with size limits
- 🗂️ Static file serving - Efficient static file handling with ETag, Range, and Last-Modified support
- 🔀 Modern routing - URLPattern-based routing
npm i dx-server jchaindx-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-polyfillThen 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 object
- getRes()- Get the current response object
All 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 buffer
- getRaw(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 parameters
Options:
{
  bodyLimit?: number      // Max body size in bytes (default: 100KB)
  urlEncodedParser?: (search: string) => any
  queryParser?: (search: string) => any
}- setJson(data, {status?, headers?})- Send JSON response
- setHtml(html, {status?, headers?})- Send HTML response
- setText(text, {status?, headers?})- Send plain text
- setBuffer(buffer, {status?, headers?})- Send buffer
- setFile(path, options?)- Send file
- setNodeStream(stream, {status?, headers?})- Send Node.js stream
- setWebStream(stream, {status?, headers?})- Send Web stream
- setRedirect(url, {status?, headers?})- Redirect response
- setEmpty({status?, headers?})- Send empty response
- makeDxContext(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 middleware
- chainStatic(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- /foobut NOT- /foo/
- '/foo/'matches- /foo/but NOT- /foo
- Use '/foo{/}?'to match both
dx-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
    })
  )
)- Use lazy body parsing - Only parse bodies when needed
- Enable compression at reverse proxy level (nginx, CDN)
- Use streaming for large responses:
import {createReadStream} from 'fs' setNodeStream(createReadStream('large-file.pdf')) 
- Cache contexts that are expensive to compute
- Use chainStaticwith 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