Skip to content

API Reference#

Imports#

typera exposes its contents in various modules, e.g. Response, Middleware, Parser, etc. In the examples below, they are imported from the top-level module like this:

import { Response, Middleware, Parser } from 'typera-express'
// or
import { Response, Middleware, Parser } from 'typera-koa'

You can also import from the individual modules:

import * as Response from 'typera-express/response'
import * as Middleware from 'typera-express/middleware'
import * as Parser from 'typera-express/parser'
// or
import * as Response from 'typera-koa/response'
import * as Middleware from 'typera-koa/middleware'
import * as Parser from 'typera-koa/parser'

And of course, you can also import individual items when importing directly from the modules:

import { BadRequest } from 'typera-express/response'
// or
import { BadRequest } from 'typera-koa/response'

Responses#

All response related types and functions live in the Response namespace.

import { Response } from 'typera-express'
// or
import { Response } from 'typera-koa'

The generic Response type looks like this:

type OptionalHeaders = { [key: string]: string } | undefined

type Response<Status, Body, Headers extends OptionalHeaders> = {
  status: Status
  body: Body
  headers: Headers
}

There is a separate type and a function to construct a response of that type for each valid HTTP status code.

For example, the response type for a 200 OK is:

type Ok<
  Body = undefined,
  Headers extends OptionalHeaders = undefined,
> = Response<200, Body, Headers>

The function to construct a 200 OK response has the following overloaded signatures:

function ok(): Ok
function ok<Body>(body: Body): Ok<Body>
function ok<Body, Headers extends OptionalHeaders>(
  body: Body,
  headers: Headers
): Ok<Body, Headers>

All response types have the Body and Headers type parameters. With other than redirect responses, both default to undefined. All response constructor functions have the same 3 signatures.

Common responses#

Here's a list of most common responses:

HTTP Type Constructor function
200 OK Ok ok
201 Created Created created
204 No Content NoContent noContent
301 Moved Permanently MovedPermanently movedPermanently
302 Found Found found
304 Not Modified NotModified notModified
400 Bad Request BadRequest badRequest
401 Unauthorized Unauthorized unauthorized
403 Forbidden Forbidden forbidden
404 Not Found NotFound notFound
405 Method Not Allowed MethodNotAllowed methodNotAllowed

For the full list of supported responses, see response.ts.

Redirects#

Redirecting the client to another URL is a common thing to do and requires setting a header. To create a redirect response, use the redirect(status, location) helper:

const myHandler: Route<Response.MovedPermanently> = route
  .get('/foo')
  .handler(async (request) => {
    return Response.redirect(301, '/bar')
  })

This generates a response with a string body and the Location header set:

HTTP/1.1 301 Moved Permanently
Location: /bar

Moved premanently. Redirecting to /bar

For simplicity, the redirecting responses listed below have the default body type of string and the default headers type of { location: string }:

HTTP Type Constructor function
301 Moved Permanently MovedPermanently redirect(301, location)
302 Found Found redirect(302, location)
303 See Other SeeOther redirect(303, location)
307 Temporary Redirect TemporaryRedirect redirect(307, location)
308 Permanent Redirect PermanentRedirect redirect(308, location)

Use the "normal" constructor functions (movedPermanently(), found(), ...) if you want full control over the body and headers.

redirect#

Signature: Response.redirect(status, location)

Creates a response that redirects to the given location. The response body will be a textual explanation of the redirect.

Streaming responses#

Use the Response.StreamingBody body type and the Response.streamingBody() function to create streaming responses. The function takes a callback that receives a writable stream as a parameter:

const streamingHandler: Route<Response.Ok<Response.StreamingBody>> = route
  .get('/document.pdf')
  .handler(async (request) => {
    return Response.ok(
      Response.streamingBody((outputStream) => {
        // Assuming that the generatePDF function generates a
        // PDF document to the given writable stream
        generatePDF(outputStream)
      })
    )
  })

Response content type#

You can set the response content type by returning a Content-Type header with the response, e.g.:

return Response.ok(csvData, { 'Content-Type': 'text/csv' })

If you don't set the Content-Type header, typera defaults to the following:

  • application/json for object and array responses. The value is encoded as JSON.
  • application/octet-stream for Buffer and streaming responses.
  • text/plain for values of primitive types (string, number, boolean). The value is converted to a string by calling value.toString().
  • No content type and an empty response body for null and undefined.

Middleware#

import * as Either from 'fp-ts/lib/Either'
import { RequestBase, Middleware, ChainedMiddleware } from 'typera-koa'
// or
import { RequestBase, Middleware, ChainedMiddleware } from 'typera-express'

Middleware are asynchronous functions that take a typera request object as a parameter, and produce either a Response or an object.

If a middleware function produces a Response, then the request handling is stopped and that response is sent to the client. If it produces an object, that object is merged to the typera request object which is passed forward to the next middleware and eventually to the route handler.

A middleware function can also add a finalizer function to be called after the request handler has finished. This is useful if the middleware allocates some resources that need to be released afterwards (e.g. release a database connection, delete a temporary file, etc.)

For example, here's a middleware that authenticates a user and adds user info to the typera request object:

const authenticateUser: Middleware.Middleware<
  // This is the object that's merged to request on success
  { user: User },
  // This is the response that is be returned by the middleware on failure
  Response.Unauthorized<string>
> = async () => {
  const user = await authenticateUser() // Gets a user somehow and returns null if unauthenticated
  if (!user) {
    return Middleware.stop(Response.unauthorized('Login first'))
  }
  return Middleware.next({ user })
}

Another example of a middleware that adds a database client to the typera request object. It never returns a response, so the response type is never.

import * as pg from 'pg'

const pool = new pg.Pool()

const db: Middleware.Middleware<
  { connection: pg.ClientBase },
  never
> = async () => {
  const connection = await pool.connect()
  return Middleware.next({ connection }, () => connection.release())
}

If you write a middleware that adds nothing to the typera request object, its result type should be unknown:

const checkOrigin: Middleware.Middleware<
  unknown,
  Response.BadRequest<string>
> = async (request) => {
  // In typera-express, request.req is the Express request
  if (request.req.get('origin') !== 'example.com') {
    return Middleware.stop(Response.badRequest('Invalid origin'))
  }
  return Middleware.next()
}

next#

Signatures:

  • Middleware.next()
  • Middleware.next(value)
  • Middleware.next(value, finalizer)

Construct a value to be merged with the typera request object, and optionally add a finalizer to be run when the request processing has finished.

If Middleware.next() is called with no arguments, nothing is added to the typera request object.

The finalizer, if given, is called with no arguments. It can be an async function (can return a Promise).

If you want to run a finalizer but not add anything to the request, you can pass {} or undefined as the value.

stop#

Signature: Middleware.stop(response)

Stop processing the request and return response to the client. Other middleware or the route handler will not be run. If other middleware have already run before this one, their finalizers are run.

ChainedMiddleware#

If you need to use the result of some previous middleware, use ChainedMiddleware. It's like Middleware but takes as first type parameter the type that previous middleware should produce. This middleware writes audit entries to database, so it requires a database connection from the db middleware above:

const audit: Middleware.ChainedMiddleware<
  { connection: pg.ClientBase },
  unknown,
  never
> = async (request) => {
  await writeAuditLog(request.connection)
  return Middleware.next()
}

Now, the audit middleware can only be used if the db middleware comes before it and adds connection to the request object.

const myRoute = route.use(db).use(audit)

Request parsers#

Request parsers are built-in middleware that let you validate parts of the request. All request parser related types and functions live in the Parser namespace.

import * as t from 'io-ts'
import { Parser } from 'typera-express'
// or
import { Parser } from 'typera-koa'

typera provides functions to build request parser middleware for query string and request body. These functions take an io-ts codec (t.Type) and return a middleware that validates the corresponding part of the request using the given codec. If the validation fails, they produce an error response with appropriate status code and error message in the body.

query#

Signature: Parser.query(codec)

Validate the query string according to the given io-ts codec. Respond with 400 Bad Request if the validation fails. The result will be available as request.query in the route handler.

The input for this parser will be the query string parsed as Record<string, string>, i.e. all parameter values will be strings. If you want to convert them to other types, you probably find the FromString codecs from io-ts-types useful (e.g. IntFromString, BooleanFromString, etc.)

body#

Signature: Parser.body(codec)

Validate the request body according to the given io-ts codec. Respond with 400 Bad Request if the validation fails. Ther result will be available as request.body in the route handler.

The input for this parser will be the request body, parsed with the body parser of your choice. With Express you probably want to use body-parser, and with Koa the most common choice is koa-bodyparser. Note that these are native Express or Koa middleware, so you must attach them directly to the Express or Koa app rather than use them as typera middleware.

Note

You must use a Express or Koa body parsing middleware for Parser.body to work.

headers#

Signature: Parser.headers(codec)

Validate the request headers according to the given io-ts codec. Respond with 400 Bad Request if the validation fails. The result will be available as request.headers in the route handler.

Header matching is case-insensitive, so using e.g. X-API-KEY, x-api-key and X-Api-Key in the codec will all read the same header. However, the parse result will of course be case sensitive. That is, the field in request.headers will have the name you specify in the io-ts codec you pass to Parser.headers, with case preserved.

The input for this parser will be the headers parsed as Record<string, string>, i.e. all header values will be strings. If you want to convert them to other types, you probably find the FromString codecs from io-ts-types useful (e.g. IntFromString, BooleanFromString, etc.)

cookies#

Signature: Parser.cookies(codec)

Validate the request cookies according to the given io-ts codec. Respond with 400 Bad Request if the validation fails. The result will be available as request.cookies in the route handler.

The input for this parser will be the cookies parsed as Record<string, string>, i.e. all cookie values will be strings. If you want to convert them to other types, you probably find the FromString codecs from io-ts-types useful (e.g. IntFromString, BooleanFromString, etc.)

Customizing the error response#

Each of the above functions also have a P flavor that allows the user to override error handling. In addition to an io-ts codec, these functions take an error handler function that receives an io-ts validation error and produces an error response:

type ErrorHandler<ErrorResponse extends Response.Response<number, any, any>> = (
  errors: t.Errors
) => ErrorResponse

function queryP<
  Codec extends t.Type<any>,
  ErrorResponse extends Response.Response<number, any, any>,
>(
  codec: Codec,
  errorHandler: ErrorHandler<ErrorResponse>
): Middleware<{ query: t.TypeOf<Codec> }, ErrorResponse>

function bodyP<
  Codec extends t.Type<any>,
  ErrorResponse extends Response.Response<number, any, any>,
>(
  codec: Codec,
  errorHandler: ErrorHandler<ErrorResponse>
): Middleware<{ body: t.TypeOf<Codec> }, ErrorResponse>

function headersP<
  Codec extends t.Type<any>,
  ErrorResponse extends Response.Response<number, any, any>,
>(
  codec: Codec,
  errorHandler: ErrorHandler<ErrorResponse>
): Middleware<{ headers: t.TypeOf<Codec> }, ErrorResponse>

function cookiesP<
  Codec extends t.Type<any>,
  ErrorResponse extends Response.Response<number, any, any>,
>(
  codec: Codec,
  errorHandler: ErrorHandler<ErrorResponse>
): Middleware<{ cookies: t.TypeOf<Codec> }, ErrorResponse>

If you want to abstract your custom error handling to reuse it in multiple routes, you can create your own parser functions like this:

import * as t from 'io-ts'

function errorToString(err: t.Errors): string {
  // Turn err to string the way you like
}

const myQuery = <T,>(
  codec: t.Type<T>
): Middleware<{ query: T }, Response.BadRequest<string>> =>
  Parser.queryP(codec, (errors) => Response.badRequest(errorToString(errors)))

// You can alse return a different response than 400 Bad Request
const myBody = <T,>(
  codec: t.Type<T>
): Middleware<{ body: T }, Response.Conflict<string>> =>
  Parser.bodyP(codec, (errors) => Response.conflict(errorToString(errors)))

// etc...

Using express middleware#

This is an experimental feature, and is currently available only for typera-express. It can change without a corresponding semver bump.

Express middleware is inherently incompatible with typera middleware, because typera runs its middlewares independently of the Express middleware. This makes it possible to pass typed data to the next middleware and the route handler.

However, lots of useful Express middleware already exists out there. typera-express has a function that helps you wrap existing Express middleware in a way that it works (mostly) like typera middleware does:

wrapNative#

Signatures:

  • Middleware.wrapNative(expressMiddleware)
  • Middleware.wrapNative(expressMiddleware, mapResult)

Given Express middleware function expressMiddleware, return the corresponding typera middleware.

If the mapResult function is given, its called after the middleware has run, and its return value is merged to the typera request object. Use this function to take any data the wrapped middleware produces and make it consumable by other typera middleware or the route handler function.

The wrapped Express middleware may either pass the control to the next middleware (or route handler) in the chain by calling next(), the third parameter of the middleware function, or send the response and end the middleware chain. Some middleware use various tricks to hook to the point where the response is eventually sent, to e.g. log info about it. wrapNative tries to make all of this possible, but there might be corner cases which don't work yet.

Routes#

import { Route, URL, applyMiddleware, route } from 'typera-express'
// or
import { Route, URL, applyMiddleware, route } from 'typera-koa'

route#

A route matches a request based on HTTP method and path, and defines a function that serves a response for the matched request.

Routes are created using the route.[method](...) or route(method, ...), where method is one of get, post, put, delete, head, options, patch or all:

route
  .get(path)
  .use(middleware1, middleware2 /*, ... */)
  .handler(async (request) => {
    // ...
    return Response.ok()
  })

The special method all matches every HTTP method.

The route functions take a path pattern as an argument. The path pattern can contain route parameter captures. The path if the incoming HTTP request is matched against the path pattern to see whether this route is responsible for serving the response for the HTTP request.

They return an object with .use() and .handler() methods.

The .use() method takes one or more middleware functions which are used to process the incoming request and create the typera request object (request). You can call .use() many times. The result of middleware in previous calls will be available in the typera request object passed to the next middleware. See ChainedMiddleware above on how to use the previous middleware results in the next middleware.

The .handler() method takes a request handler, which is an async function that receives the typera request object returns a response.

The typera request object is created by merging the route parameters and the result objects of middleware functions given to route or applied before. It also always extends the request base:

// typera-express
export type RequestBase = {
  req: express.Request
  res: express.Response
}

// typera-koa
export type RequestBase = {
  ctx: koa.Context
}

In other words, in typera-express the Express req/res are always available as request.req and request.res, and in typera-koa the Koa context is always available as request.ctx.

The type of request is inferred by typera, so there's no need for the user to give it an explicit type, while at the same time the TypeScript compiler checks that the properties of request are used correctly in the request handler.

route infers the response type by combining the error response types of all middleware functions, and the response types of the request handler. To get maximum type safety, you should explicitly declare the return type of route in your code. This makes sure that the possible responses of a route don't change unexpectedly because of changes in the code, and documents all the possible responses from a single route:

const listHandler: Route<Response.Ok<User> | Response.BadRequest<string>> =
  route
    .get(/* ... */)
    .use(/* ... */)
    .handler(async (request) => {
      // ...
    })

We avoid giving the accurate type of the various route functions here, because they're quite complex due to the type inference of request and response types. Interested readers can refer to the code: common, express, koa,

route.use#

applyMiddleware#

Signatures:

  • route.use(...middlewares)
  • applyMiddleware(...middlewares)

If you need to apply the same middleware to many routes, you can create your own version of route by calling either route.use() or applyMiddleware() with the middleware that are common to all of the routes:

// db and auth are middleware functions
const myRoute = route.use(db, auth)
// or
const myRoute = route.use(db).use(auth)
// or
const myRoute = applyMiddleware(db, auth)

const listHandler: Route<...> = myRoute.get(...)
const updateHandler: Route<...> = myRoute.put(...)

The value returned by route.use() and applyMiddleware() works exactly the same as route i.e. it has the .get(), .post() etc. methods and can be called directly.

Route parameter capturing#

Path patterns make it possible to extract some parts of the HTTP request path for use in the route handler.

For example, with the following path:

route.get('/user/:id(int)').handler(async (request) => { ... })

In the route handler function, req.routeParams.id will contain the integer that was given after /user/, like this:

Path req
/user/5 { routeParams: { id: 5 }}
/user/528472 { routeParams: { id: 528472 }}
/user/foo Route is not matched
/user/5/ Route is not matched
/user/ Route is not matched

Route parameters have the syntax :name or :name(conv), where the optional (conv) specifies a conversion to be applied to the parameter. Without a conversion, the parameter is captured as a string.

Parameter names should only contain the a-z, A-Z and _ characters. They can be separated with - and ., so these are valid path patterns:

  • /flights/:from-:to
  • /plantae/:genus.:species

One built-in conversion is available: (int) converts the parameter to a (non-negative) integer, or fails to match if something else than an integer is supplied.

useParamConversions#

Signature: route.useParamConversions({ ...convs })

import * as Option from 'fp-ts/lib/Option

You can register your own conversions by calling useParamConversions. It has one argument, an object of { name: conversion }, where name specifies the name of the conversion and conversion is a function (value: string) => Option.Option<T>. If the function returns a some, the value will be available under the name key in request.routeParams. If it returns a none, the route will return a 404 Not Found response.

The value returned by route.useParamConversions()works exactly the same as route i.e. it has the .get(), .post() etc. methods and can be called directly.

Example:

const silly: URL.Conversion<boolean> = (value) => Option.some(value === 'silly')

const funny: URL.Conversion<number> = (value) =>
  value === 'funny' ? Option.some(42) : Option.none

const myRoute = route.useParamConversions({ silly, funny })

const funnyRoute = myRoute
  .get('/foo/:param(silly)/:other(funny)')
  .handler((request) => {
    // request.routeParams is { silly: boolean, funny: number }
  })

Router#

import { router } from 'typera-express'
// or
import { router } from 'typera-koa'

The router is used to take a bunch of routes and turn them into a handler that you can then attach to your Express or Koa app.

Available functions:

  • router(...routes)
  • r.add(...routes)
  • r.handler()

Use the router() function to create a router. Give it zero or more routes to add.

The .add() method adds more routes to the router. Note that it returns a new Router instance instead of modifying the existing one.

The .handler() method returns a handler that can be passed to app.use() for both Express and Koa.

With Express, you can mount the handler to a sub-path like this:

import * as express from 'express'

const app = express()
// ...

app.use('/subpath', router.handler())

With Koa, you need to use koa-mount to mount your routes to a sub-path:

import * as Koa from 'koa'
import mount = require('koa-mount')

const app = new Koa()
// ...

app.use(mount('/subpath', router.handler()))