ohz
v0.1.2
Published
An Observable-based HTTP server
Downloads
3
Readme
Obvi
An HTTP server based on Observables
Usage
To see an example server running, install the repo ( yarn install
), and then run the server ( yarn dev
). This will start the server at the example.ts
file.
Below is a stripped down version of using ohz
with a vanilla server:
const { router, json, server: createServer } = require('ohz')
const server = createServer()
server
.use(json())
.use(router.route('/posts/:id', ctx => ({
...ctx,
body: {
query: ctx.request.query,
param: ctx.request.params
}
})))
.listen(5000, () => console.log('listening'))
Below is a vanilla JavaScript usage example with detailed comments:
// We are going to be using the map operator
// to make our middleware easier to write
const { of } = require('rxjs')
const { map } = require('rxjs/operators')
// We also want to import some middleware (router, json),
// along with our server creator
const { router, json, server: createServer } = require('ohz')
// We create a server instance
// It has some basic generics that
// can get us up and running quickly
// so if you just want to see how it works
// you can do
//
// server = createServer()
//
// without passing a configuration object
const server = createServer({
// but if you want to get specific, we can
// inject our own handlers into the system.
//
// createContext is the first lego piece in our
// system. It takes in a Subject<Context> and
// returns a function that takes in (http.ClientRequest, http.ServerResponse)
// and emits an event into the Subject, with a value of Context.
//
// The `headers` value is not setting the _incoming_ headers
// but setting the _outgoing_ headers.
//
// Below, we take in a generic subject and emit a generic context
// but if you need to do more here, be sure that you include the
// parsing of url and query or else router and other downstream
// values will not behave as expected!
createContext: sub => (request, response) => {
const parsedUrl = url.parse(request.url)
Object.assign(request, {
url: parsedUrl, // { pathname: '/path/without/query' }
query: parsequery(parsedUrl.search) // { query: args }
})
sub.next({
request,
response,
headers: {},
body: null
})
},
// This is mostly a placeholder. As of now, the default
// is the return an empty object. The idea is that you
// can add values to the `server` that is returned
// each time you call `server.use` or `server.remove`
// or to override `use` and `remove` yourself.
//
// I probably wouldn't use this but it's nice to have the
// option to!
createResult: () => ({})
})
server
// Just like connect, we can call `use` to add middleware
// to our server instance. They take a different signature
// though! Instead of `(ctx, next) => void` or
// `(req, res, next)` => void,
// this middleware is of type
// (ctx) => Observable<Context>.
//
// We have included some basic middlewares in order to
// get some basic boilerplate taken care of. `json` is
// a module that takes in nothing and returns middleware
// that handles incoming JSON data along with outgoing
//
// It does assume that the end-user is calling `send`
// added by this middleware. If you change your subscribeFn
// when you `listen`, you might get different results.
.use(json())
// We also have a way to add routes based on express-like urls
// router.route takes in an express-like url and a handler. The
// handler can either return a new Context value or return an
// Observable
//
// router.route will handle _all_ methods to that route.
.use(router.route('/users', (ctx) => {
// Here we return `of(...)` to show
// that you can return Observables.
// Remove `of` and just return a new
// context value. It will work the same!
return of({
...ctx,
// We set the `body`
// of the Context for the downstream
// handlers to care about
body: {
data: [
{
_id: 1,
// We can also read from the incoming
// requests. This is assuming that the
// client has added a JSON body and it
// has a user value
name: ctx.request.body.user
}
]
}
})
}))
// If you want to only respond to get requests,
// we can use router.get, which also takes a
// route and handler but will only respond
// to get requests to that endpoint
.use(router.get('/posts', (ctx) => ({
...ctx,
body: {
data: true
}
})))
// Just like express, we can use named parameters
// in our routes. This will be called when we have
// the url /posts/1234.
//
// As you can see in the body of this handler, we
// have access to the params ( such as id ), along
// with they query ( )
.use(router.get('/posts/:id', (ctx) => ({
...ctx,
body: {
data: 'you did it!',
meta: {
params: ctx.request.params,
query: ctx.request.query
}
}
})))
// We can also add generic Pipe-able functions
// by using rxjs/operators. map(fn) is the same
// as writing a function of Obs => Obs.map(fn)
.use(map(({ body, headers, ...args }) => {
if (!body) {
return ({
...args,
body: JSON.stringify({
error: {
message: 'Route not found'
}
}),
headers: {
...headers,
code: 404,
'Content-Type': 'application/json'
}
})
}
return ({
body,
headers,
...args,
})
}))
// Once we call listen, we can no longer add/remove middleware
//
// We give listen a port, callback function, and subscribe function
//
// port is the port to listen on
// callback is the function to be called when the server is
// actually listening on that port
// subscribe is a function that takes a Context and responds
// to the eventual value
.listen(
// Some random port will do, as long as it's not in use
5000,
// We just want to know when the server is ready!
() => console.log('Listening at http://localhost:5000'),
// This is the default subscription function.
//
// We just take the context, try to use some `send`
// method if the upstream set it ( like json() does )
// or we write the headers, set the code, and write the
// body to the client, ending the conneciton.
//
// If you want to do something differently, you can change this
// subscription function and handle the response however
// you need to. If you just want to be able to set a string as
// the value to be sent, don't include anything. If you want
// to set JSON-able data, use the `json()` middleware as above.
({ request, response, body, headers = {} }) => {
// the user has a custom handler
if (response.send) {
response.send(headers, body)
} else {
// default to string
// Set headers
response.writeHead(headers.code || 200, headers)
// Assume middleware took care of the body
response.end(body)
}
}
)