dx-server - modern, unopinionated, and satisfactory server
Install
yarn add dx-server jchain
Usage
Check below sample with comment for more details.
Simple 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),
next => {
getRes().setHeader('Cache-Control', 'no-cache')
console.log(getReq().method, getReq().url)
return next()
},
async next => {
try {await next()} catch (e) {
console.error(e)
setHtml('internal server error (code: internal)', {status: 500})
}
},
router.get({
'/'() {setHtml('hello world')},
'/health'() {setText('ok')}
}),
() => {setHtml('not found', {status: 404})},
)()
).listen(3000, () => console.log('server is listening at 3000'))
More complex server with express.
This sample additionally requires: yarn install express morgan
import {Server} from 'node:http'
import {promisify} from 'node:util'
import chain from 'jchain'
import dxServer, {
getReq, getRes,
getJson, getRaw, getText, getUrlEncoded, getQuery,
setHtml, setJson, setText, setBuffer, setRedirect, setNodeStream, setWebStream,
router, connectMiddlewares
} from 'dx-server'
import {expressApp} from 'dx-server/express'
import express from 'express'
import morgan from 'morgan'
import {AsyncLocalStorage} from 'node:async_hooks'
class ServerError extends Error {
name = 'ServerError'
constructor(message, status = 400, code = 'unknown') {
super(message)
this.status = status
this.code = code
}
}
const authStorage = new AsyncLocalStorage()
const authChain = async next => {
const auth = getReq().headers.authorization ? {id: 1, name: 'joe'} : undefined
return authStorage.run(auth, next)
}
const requireAuth = () => {
if (!authStorage.getStore()) throw new ServerError('unauthorized', 401, 'unauthorized')
}
const serverChain = chain(
next => {
getRes().setHeader('Cache-Control', 'no-cache')
return next()
},
async next => {
try {
await next()
} catch (e) {
if (e instanceof ServerError) setHtml(`${e.message} (code: ${e.code})`, {status: e.status})
else {
console.error(e)
setHtml('internal server error (code: internal)', {status: 500})
}
}
},
connectMiddlewares(
morgan('common'),
),
await expressApp(app => {
app.set('trust proxy', true)
if (process.env.NODE_ENV !== 'production') app.set('json spaces', 2)
app.use('/public', express.static('public'))
}),
authChain,
router.post({
async '/api'({next}) {
try {
await next()
} catch (e) {
if (e instanceof ServerError) setJson({
error: e.message,
code: e.code,
}, {status: e.status})
else {
console.error(e)
setJson({
message: 'internal server error',
code: 'internal'
}, {status: 500})
}
}
}
}, {end: false}),
router.post({
'/api/sample-public-api'() {
setJson({name: 'joe'})
},
'/api/me'() {
requireAuth()
setJson({name: authStorage.getStore().name})
},
}),
router.get({
'/'() {
setHtml('ok')
},
'/health'() {
setHtml('ok')
}
}),
() => {
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'}),
serverChain,
)()
} catch (e) {
console.error(e)
}
})
await promisify(tcpServer.listen.bind(tcpServer))(3000)
console.log('server is listening at 3000')
TODO
Until these middlewares are available as native dx-server middlewares, express middlewares can be used with expressApp()
Note:
getBuffer, getJson, getRaw, getText, getUrlEncoded, getQuery
are all synchronous functions.
The associated results are calculated in the first time they are called and cached for subsequent calls.
If you want to get these values synchronously, you can do as follows:
import {AsyncLocalStorage} from 'node:async_hooks'
import {getJson} from 'dx-server'
const jsonStorage = new AsyncLocalStorage()
chain(
async next => jsonStorage.run(await getJson(), next),
next => {
console.log(jsonContext.value)
return next()
}
)