🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
Book a DemoInstallSign in
Socket

dx-server

Package Overview
Dependencies
Maintainers
1
Versions
59
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dx-server

A modern, unopinionated, and performant Node.js server framework built on AsyncLocalStorage for elegant API interfaces.

0.10.2
latest
Source
npm
Version published
Weekly downloads
43
-82.66%
Maintainers
1
Weekly downloads
 
Created
Source

dx-server

A modern, unopinionated, and performant Node.js server framework built on AsyncLocalStorage for elegant API interfaces.

npm version License: MIT

Features

  • 🎯 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

Installation

npm i dx-server jchain

URLPattern Support

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')
}

Quick Start

Basic Server

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'))

TypeScript Example

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)

Static File Server

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)

Production-Ready Server with Express Integration

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')

Core Concepts

Context-Based Architecture

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')
}

Lazy Body Parsing

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()
  }
)

Custom Contexts

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()
  }
)

API Reference

Main Exports

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'

Core Functions

Request/Response Access

  • getReq() - Get the current request object
  • getRes() - Get the current response object

Body Parsers

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
}

Response Setters

  • 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

Context Management

  • 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)
    

Middleware Utilities

  • 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
    })
    

Routing

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

URLPattern vs Express Patterns

PatternURLPatternExpress
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
  • Use '/foo{/}?' to match both

Express Integration

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'})
    })
  })
)

Low-Level Helpers

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)

Security Considerations

Body Size Limits

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
)

Error Handling

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})
      }
    }
  }
)

Input Validation

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...
})

Security Headers

Use security middleware:

import helmet from 'helmet'
import cors from 'cors'

chain(
  connectMiddlewares(
    helmet(),
    cors({
      origin: process.env.ALLOWED_ORIGINS?.split(','),
      credentials: true
    })
  )
)

Advanced Examples

File Upload with Busboy

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)
})

WebSocket Upgrade

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)
    })
  }
})

Rate Limiting

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
    })
  )
)

Performance Tips

  • 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 chainStatic with proper cache headers for static assets

Migration from Express

// 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})
})

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT © Sang Tran

FAQs

Package last updated on 01 Jul 2025

Did you know?

Socket

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.

Install

Related posts