A partial porting of https://github.com/owickstrom/hyper to TypeScript
hyper-ts
is an experimental middleware architecture for HTTP servers written in TypeScript.
Its main focus is correctness and type-safety, using type-level information to enforce correct composition and
abstraction for web servers.
Goals
The goal of hyper-ts
is to make use of type system features in TypeScript to enforce correctly stacked middleware in
HTTP server applications. All effects of middleware should be reflected in the types to ensure that common mistakes
cannot be made. A few examples of such mistakes could be:
- Incorrect ordering of header and body writing
- Writing incomplete responses
- Writing multiple responses
- Trying to consume a non-parsed request body
- Consuming a request body parsed as the wrong type
- Incorrect ordering of, or missing, error handling middleware
- Incorrect ordering of middleware for sessions, authentication, authorization
- Missing authentication and/or authorization checks
TypeScript compatibility
hyper-ts version | fp-ts version | typescript version |
---|
0.7.x+ | 2.10.5+ | 4.3+ |
0.5.x+ | 2.0.5+ | 3.5+ |
0.4.x+ | 1.15.0+ | 3.0.1+ |
Hello world
import * as express from 'express'
import * as H from 'hyper-ts'
import * as M from 'hyper-ts/Middleware'
import { toRequestHandler } from 'hyper-ts/express'
import { pipe } from 'fp-ts/function'
const hello: M.Middleware<H.StatusOpen, H.ResponseEnded, never, void> = pipe(
M.status(H.Status.OK),
M.ichain(() => M.closeHeaders()),
M.ichain(() => M.send('Hello hyper-ts on express!'))
)
express()
.get('/', toRequestHandler(hello))
.listen(3000, () => console.log('Express listening on port 3000. Use: GET /'))
Sending a JSON
const hello = pipe(
M.status(H.Status.OK),
M.ichain(() => M.json({ a: 1 }, () => 'error'))
)
Core API
Connection
A Connection
models the entirety of a connection between the HTTP server and the user agent, both
request and response.
State changes are tracked by the phantom type S
.
interface Connection<S> {
readonly _S: S
readonly getRequest: () => IncomingMessage
readonly getBody: () => unknown
readonly getHeader: (name: string) => unknown
readonly getParams: () => unknown
readonly getQuery: () => unknown
readonly getOriginalUrl: () => string
readonly getMethod: () => string
readonly setCookie: (
this: Connection<HeadersOpen>,
name: string,
value: string,
options: CookieOptions
) => Connection<HeadersOpen>
readonly clearCookie: (this: Connection<HeadersOpen>, name: string, options: CookieOptions) => Connection<HeadersOpen>
readonly setHeader: (this: Connection<HeadersOpen>, name: string, value: string) => Connection<HeadersOpen>
readonly setStatus: (this: Connection<StatusOpen>, status: Status) => Connection<HeadersOpen>
readonly setBody: (this: Connection<BodyOpen>, body: unknown) => Connection<ResponseEnded>
readonly endResponse: (this: Connection<BodyOpen>) => Connection<ResponseEnded>
}
By default hyper-ts
manages the following states:
StatusOpen
: Type indicating that the status-line is ready to be sentHeadersOpen
: Type indicating that headers are ready to be sent, i.e. the body streaming has not been startedBodyOpen
: Type indicating that headers have already been sent, and that the body is currently streamingResponseEnded
: Type indicating that headers have already been sent, and that the body stream, and thus the response, is finished
During the connection lifecycle the following flow is statically enforced
StatusOpen -> HeadersOpen -> BodyOpen -> ResponseEnded
Note. hyper-ts
supports express 4.x by default by exporting a Connection
instance from the hyper-ts/express
module.
Middleware
A middleware is an indexed monadic action transforming one Connection
to another Connection
. It operates in the TaskEither
monad,
and is indexed by I
and O
, the input and output Connection
types of the middleware action.
interface Middleware<I, O, E, A> {
(c: Connection<I>): TaskEither<E, [A, Connection<O>]>
}
The input and output type parameters are used to ensure that a Connection
is transformed, and that side-effects are
performed, correctly, throughout the middleware chain.
Middlewares are composed using chain
and ichain
, the indexed monadic version of chain
.
Type safety
Invalid operations are prevented statically
import { Status, status } from 'hyper-ts'
pipe(
M.status(H.Status.OK),
M.ichain(() => M.header('name', 'value')),
M.ichain(() => M.closeHeaders()),
M.ichain(() => M.send('Hello hyper-ts on express!')),
M.ichain(() => M.header('name', 'value'))
)
No more "Can't set headers after they are sent."
errors.
Decoding params, query and body
Input validation/decoding is done by defining a decoding function with the following signature
(input: unknown) => Either<L, A>
Example (decoding a param)
import * as H from 'hyper-ts'
import * as M from 'hyper-ts/Middleware'
import * as E from 'fp-ts/Either'
const isUnknownRecord = (u: unknown): u is Record<string, unknown> => typeof u === 'object' && u !== null
export const middleware: M.Middleware<H.StatusOpen, H.StatusOpen, string, string> = M.decodeParam('user_id', u =>
isUnknownRecord(u) && typeof u.user_id === 'string' ? E.right(u.user_id) : E.left('cannot read param user_id')
)
You can also use io-ts decoders.
import * as H from 'hyper-ts'
import * as M from 'hyper-ts/Middleware'
import * as t from 'io-ts'
export const middleware2: M.Middleware<H.StatusOpen, H.StatusOpen, t.Errors, string> = M.decodeParam(
'user_id',
t.string.decode
)
Here I'm using t.string
but you can pass any io-ts
runtime type
import * as H from 'hyper-ts'
import * as M from 'hyper-ts/Middleware'
import { IntFromString } from 'io-ts-types/IntFromString'
export const middleware3: M.Middleware<
H.StatusOpen,
H.StatusOpen,
t.Errors,
t.Branded<number, t.IntBrand>
> = M.decodeParam('user_id', IntFromString.decode)
Multiple params
import * as H from 'hyper-ts'
import * as M from 'hyper-ts/Middleware'
import * as t from 'io-ts'
export const middleware = M.decodeParams(
t.strict({
user_id: t.string,
user_name: t.string
}).decode
)
Query
import * as H from 'hyper-ts'
import * as M from 'hyper-ts/Middleware'
import * as t from 'io-ts'
export const middleware = M.decodeQuery(
t.strict({
order: t.string,
shoe: t.strict({
color: t.string,
type: t.string
})
}).decode
)
Body
import * as H from 'hyper-ts'
import * as M from 'hyper-ts/Middleware'
import * as t from 'io-ts'
export const middleware = M.decodeBody(t.string.decode)
Here's an example using the standard express.json
middleware
Documentation
Ecosystem