An approach to async middleware for raw `http` in Node.
const http = require('http'); | |
const methods = [ 'get', 'put', 'post', 'delete', 'head' ]; | |
const isStream = obj => | |
obj && | |
typeof obj === 'object' && | |
typeof obj.pipe === 'function'; | |
const isReadable = obj => | |
isStream(obj) && | |
typeof obj._read === 'function' && | |
typeof obj._readableState === 'object'; | |
const isNext = Symbol('isNext'); | |
const composeWithContext = (...mw) => (...args) => | |
new Promise(async (resolve, reject) => { | |
const nxt = args[args.length - 1][isNext] ? args.pop() : () => resolve(); | |
await mw.reduceRight((next, curr) => | |
async function() { | |
next[isNext] = true; | |
try { await curr(...args.concat(next)) } | |
catch(e) { reject(e); throw e; } | |
}, nxt)(); | |
resolve(); | |
}); | |
const resolve = async (req, res, next) => { | |
await next(); | |
if (isReadable(res.body)) { | |
res.body.pipe(res); | |
} else { | |
res.end(res.body || ''); | |
} | |
}; | |
const route = (path, fn) => async (req, res, next) => { | |
if (req.url === path) { | |
await composeWithContext(fn, next)(req, res); | |
} else { | |
await next(); | |
} | |
}; | |
const method = (me, fn) => async(req, res, next) => { | |
if (req.method.toLowerCase() === me) { | |
await composeWithContext(fn, next)(req, res); | |
} else { | |
await next(); | |
} | |
}; | |
const nomad = () => { | |
const mw = [ resolve ]; | |
const app = { | |
use: (path, fn) => { | |
if (!fn && typeof path === 'function') return (mw.push(path), app); | |
if (typeof path === 'string' && typeof fn === 'function') return (mw.push(route(path, fn)), app); | |
}, | |
listen: (port, callback = () => {}) => { | |
http.createServer(composeWithContext(...mw)).listen(port, callback); | |
} | |
}; | |
return methods.reduce((a, me) => { | |
a[me] = (path, fn) => { | |
if (!fn && typeof path === 'function') return (mw.push(method(me, path)), app); | |
if (typeof path === 'string' && typeof fn === 'function') return (mw.push(route(path, method(me, fn))), app); | |
} | |
return a; | |
}, app); | |
}; | |
export default nomad; |
import fs from 'fs'; | |
import nomad from './core'; | |
const PORT = 1337; | |
let count = 0; | |
const stats = file => | |
new Promise((resolve, reject) => { | |
fs.stat(file, (err, stat) => err ? reject(err) : resolve(stat)); | |
}); | |
const assets = path => async (req, res, next) => { | |
try { | |
const filePath = `${path}${req.url}`; | |
const stat = await stats(filePath); | |
if (!stat.isFile()) throw new Error('Cannot load directory'); | |
res.statusCode = 200; | |
res.body = fs.createReadStream(filePath); | |
} catch(e) { | |
await next(); | |
} | |
} | |
const app = nomad(); | |
app.use(async (req, res, next) => { | |
const time = Date.now(); | |
console.log(`initiate req ${++count}`); | |
await next(); | |
res.setHeader('Response-Time', `${Date.now() - time}ms`); | |
}); | |
app.use(async (req, res, next) => { | |
try { await next() } | |
catch(e) { | |
res.statusCode = 500; | |
res.body = e.message; | |
} | |
}); | |
app.get(assets(process.cwd())); | |
app.use('/hello', (req, res, next) => { | |
res.statusCode = 200; | |
res.body = 'hello'; | |
}); | |
app.use('/world', (req, res, next) => { | |
res.statusCode = 200; | |
res.body = 'world'; | |
}); | |
app.get('/file', (req, res, next) => { | |
res.statusCode = 200; | |
res.body = fs.createReadStream(`${process.cwd()}/package.json`); | |
}); | |
app.use(() => { | |
throw new Error('Route not found.'); | |
}); | |
app.listen(PORT, () => console.log(`Server started on: ${PORT}`)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment