A library for creating HTTP services. It’s mostly inspired by http4s, as well as Express.js, Spring Boot. Aims to be convenient, type-safe, performant, and testable. Takes a lot of concepts from functional programming, but does not force you to deep into complicated concepts.
These are very early concepts of how code might be looks like
Version: 0.1
const jsonDecoder = (r: Request) =>
r.flatMap(r => r.body.as(Decoder.JSON).then(body => r.set('body', body)))
const rootService = Service('/')
.get().path('bad').handler(() => BadRequest('Bad'))
.get().path('ok').handler(() => Ok('OK'))
.post().path('post').handler(() => Ok())
.post().path('echo').handler(r => r.body.as('string').then(Ok))
.get().path('b/c').handler(() => Ok())
.post().path('query').query('name', QueryDecoder.string).handler(r => Ok(`Hello ${r.query.name}!`))
.get().path('wait').handler(() => sleep(10).then(Ok))
.get().path('boom').handler(Promise.reject)
.post().path('reverse').handler(r => r.body.as(Decoders.String).then(str => str.reverse()).then(Ok))
.post.path('body').use(jsonDecoder).handler(r => Ok(r.body))
Version: 0.2. A full scale appication for managing a restaurant:
import { Service } from 'n_h'
import * as Decoders from 'n_h/decoders'
import { Ok, Unauthorized } from 'n_h/response'
import { Schema, t } from 'n_h/schema'
import * as Middleware from 'n_h/middleware'
import { connect } from 'src:/database'
const corsPolicy = Middleware.CORS()
.withAllowOrigin(process.env.CLIENT_DOMAIN)
.withAllowMethodsAll()
.withAllowCredentials(true)
.withAllowHeaders(new Set('Content-Type'))
const session = Middleware.Session()
const rootService = Service('/').use(corsPolicy, sessionMiddleware)
// Works via Middleware.mapRequest
// Automatically strictly preserves types
const loginBodyDecoder = Decoders.JSON.fromSchema(
Schema(t.struct({
email: t.string,
password: t.string,
}))
).toMiddleware()
const connectDatabaseMiddleware = Middleware.mapRequest(
// Create new request and set a 'db' parameter
req => connect().then(db => req.set('db', db))
)
const authService = rootService
// Create a bunch of POST handlers, parse body for each according to decoder middleware
.post().use(loginBodyDecoder)
// POST:/registration
.path('registration').handler(
(req) => {
const user = await addUser(db, req.body)
await updateSession(db, req.session.id)
return Ok()
}
)
// POST:/login
.path('/login').handler(
(req) => {
const { body } = req
const user = await getUser(req.db, body.email, body.password)
if (user) {
await updateSession(req.db, req.session.id)
return Ok(user)
} else {
return Unauthorized('no such user')
}
}
)
const productBodyDecoder = Decoders.FormData.fromScheme(
Schema(t.struct({
title: t.string,
description: t.string,
price: t.string,
image: t.binaryStream,
}))
)
const productService = rootService
.route('products')
.get().handler(
() => ProductModel.getAllProducts().then(Ok).catch(NotFound)
)
.get().param('id', Decoders.number).handler(
(req) => ProductModel.getProduct(req.id).then(Ok).catch(NotFound)
)
.post().use(productBodyDecoder).handler(
async (req) => {
const { image, title, description, price } = req.body
const path = await ImageDomain.createImage(image)
return Ok(ProductDomain.createProduct({ title, description, price, image: path }))
}
)
const app = Service.combine(authService, productService)
Server()
.withPort(8081)
.withService(app)
.start()
The main important thing is a Service. The service is a pure data structure. It is not strictly bound with the real HTTP request but rather just takes some request-like input, and produces a response-like answer. The library user is not concerned about it and just uses a declarative interface. Services might be merged together, and then a dedicated structure, a Server, takes service and opens real HTTP connections.
When I'm talking about service as a data structure, I mean a tree, something similar to prefix-tree, where every node has some context, some index (url part), and some middleware that must be executed to go forward.