typeraType-safe routes for Express and Koa
typera - Type-safe routes
Typera (TYPEd Routing Assistant) helps you build backends in a type-safe manner by leveraging io-ts and some TypeScript type inference magic. It works with both Express and Koa.
Upgrading to version 2? See the upgrading instructions.
Table of Contents
- The Problem
- Tutorial
- Requirements
- Tools
- API Reference
- Imports
- Responses
- Redirects
- Streaming responses
- Middleware
- Request parsers
Parser.query<T>(codec: t.Type<T>): Middleware<{ query: T }, Response.BadRequest<string>>
Parser.body<T>(codec: t.Type<T>): Middleware<{ body: T }, Response.BadRequest<string>>
Parser.headers<T>(codec: t.Type<T>): Middleware<{ headers: T }, Response.BadRequest<string>>
Parser.cookies<T>(codec: t.Type<T>): Middleware<{ cookies: T }, Response.BadRequest<string>>
- Customizing the error response
- Using express middleware
- Routes
-
route
route.get(...): Route<Response>
route.post(...): Route<Response>
route.put(...): Route<Response>
route.delete(...): Route<Response>
route.head(...): Route<Response>
route.options(...): Route<Response>
route.patch(...): Route<Response>
route.all(...): Route<Response>
route(method, ...): Route<Response>
route.use(...middleware)
applyMiddleware(...middleware)
-
- Route parameter capturing
- Router
- Upgrading
- Releasing
The Problem
When you see an any
, you cannot really be sure anymore. When building web backends, there are quite a few any
s involved:
-
You get a request in. Captured route params, query params from the URL and request body are all
any
. -
When generating a response, the response body's type is
any
. -
When middleware is involved, there's no type-level visibility to which transforms the middleware apply to the request, or which responses it might return.
-
The response status is a
number
. It's not as bad asany
, but your routes always return responses from a known set of possible status code / body combinations.
By default, the compiler cannot help you with any (pun intended) of this. But with typera, you're safe!
Tutorial
Install with yarn or npm.
For Express:
yarn add express typera-express
# or
npm install --save express typera-express
For Koa:
yarn add typera-koa
# or
npm install --save koa typera-koa
Here's an example of a typed route handler that updates a user's profile in the database:
// Change 'typera-express' to 'typera-koa' if you're using Koa
import { Parser, Response, Route, URL, route } from 'typera-express'
import * as t from 'io-ts'
interface User {
id: number
name: string
age: number
}
// Decodes an object { name: string, age: number }
const userBody = t.type({ name: t.string, age: t.number })
const updateUser: Route<
Response.Ok<User> | Response.NotFound | Response.BadRequest<string>
> = route
.put('/user/:id(int)') // Capture id from the path
.use(Parser.body(userBody)) // Use the userBody decoder for the request body
.handler(async (request) => {
// This imaginary function takes the user id and data, and updates the
// user in the database. If the user does not exist, it returns null.
const user = await updateUserInDatabase(
request.routeParams.id,
request.body
)
if (user === null) {
return Response.notFound()
}
return Response.ok({
id: user.id,
name: user.name,
age: user.age,
})
})
Let's go through it in detail.
// Change 'typera-express' to 'typera-koa' if you're using Koa
import { Parser, Response, Route, URL, route } from 'typera-express'
interface User {
id: number
name: string
age: number
}
// Decodes an object { name: string, age: number }
const userBody = t.type({ name: t.string, age: t.number })
We first import the stuff that is needed, and define an object type that is returned from the route handler. We also define an io-ts codec for decoding incoming user data.
const updateUser: Route<
Response.Ok<User> | Response.NotFound | Response.BadRequest<string>
> = route /* ... */
Then we declare our route's possible responses: 200 OK
with User
as a body, 404 Not Found
, or 400 Bad Request
with a string
body.
The types in the Response
namespace correspond to HTTP status codes, and their type parameter denotes the type of the response body. All the standard statuses are covered, and you can also have custom ones like this: Response.Response<418, string>
You don't need to provide the response types because typera will infer them for you. But annotating helps you catch bugs if you accidentally change the result data of a route. By annotating your route with what you actually wanted to return, you let the compiler notice if reality doesn't match the expectations.
route
.put('/user/:id(int)') // Capture id from the path
.use(Parser.body(userBody)) // Use the userBody decoder for the request body
.handler(async (request) => {
/* ... */
})
Here we tell that our route is going to handle PUT
requests. The argument of route.put()
is the path pattern. Parts of the path can be captured, like :id(int)
above (more on that later).
The .use()
method adds a middleware to the route. The userBody
io-ts codec was defined above, and passing it to the Parser.body()
middleware instructs typera to parse the incoming request body with that codec.
The .handler()
method adds the actual route logic, and here is where the magic happens. The function passed to .handler()
gets as an argument the request
object, that will contain all path captures as well as all the results of the middleware you passed. And what's great is that the data is correctly typed!
In our example, request
will have the following inferred type:
interface MyRequest {
routeParams: {
// These are the path captures, `:id(int)` in this case
id: number
}
body: {
// This is the output of the userBody decoder
name: string
age: number
}
// With typera-express
req: express.Request
res: express.Response
// With typera-koa
//ctx: koa.Context
}
(In reality the type won't be exatly as above, but a bit more complex intersection type instead. In any case, it can be used as if it was like above, editor autocomplete will work correctly, etc.)
The last part is the actual route logic:
route
.put(/*...*/)
.use(/*...*/)
.handler(async (request) => {
// This imaginary function takes the user id and data, and updates the
// user in the database. If the user does not exist, it returns null.
const user = await updateUserInDatabase(
request.routeParams.id,
request.body
)
if (user === null) {
return Response.notFound()
}
return Response.ok({
id: user.id,
name: user.name,
age: user.age,
})
})
The above code returns either 200 OK
with the user data in the body, or 404 Not Found
without any body, depending on whether the user was found in the database or not.
Note that the OK response body corresponds to the User
type we defined earlier. We annotated the route handler to return a User
body with the OK response.
Let's assume we made a typo in our code and wrote ic
instead of id
:
return Response.ok({ ic: user.id, name: user.name, age: user.age })
// OOPS! -------------^
The TypeScript compiler catches this and gives you an error. Likewise, if in the future someone changes the updateUserFromDatabase
function and e.g. adds a new field to the user data that the function takes as the second argument, the code won't compile before they also fix the userBody
decoder to match the new type.
It's not required to use the response helpers like Response.ok()
or Response.notFound()
. You can also return plain objects: return { status: 200, body: { ... }, headers: { ... } }
Did you notice that the updateUser
route handler also had a Response.BadRequest<string>
as a possible response even though the route logic never returns such a response? This is because the validation of the request body can fail. The Parser.body()
middleware produces a 400 Bad Request
response if the request body doesn't pass validation, and this response type is included as one of the possible response types of the route.
There's one piece still missing: adding our route handlers to the app. Use the router()
function to create a router from a bunch of routes, and the .handler()
method of the router to get a handler that can be added to the app.
Here's an example for Express:
import * as express from 'express'
import * as bodyParser from 'body-parser'
import { router } from 'typera-express'
const app = express()
// body-parser is needed if you use Parser.body()
app.use(bodyParser.json())
app.use(router(updateUser /*, otherRoute, stillAnother */).handler())
And for Koa:
import * as Koa from 'koa'
import * as bodyParser from 'koa-bodyparser'
import { router } from 'typera-koa'
const app = new Koa()
// koa-bodyparser is needed if you use Parser.body()
app.use(bodyParser())
app.use(router(updateUser /*, otherRoute, stillAnother */).handler())
Requirements
Requires TypeScript 4.1 or newer, and Node 12 or newer.
Tools
- typera-openapi: Automatically generates OpenAPI definitions for your routes
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-koa/express'
// 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.
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.
Response.redirect<Status>(status: Status, location: string): Response.Response<Status, string, { location: string }>
Create 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)
})
)
})
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()
}
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.
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.
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.
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.
Parser.query<T>(codec: t.Type<T>): Middleware<{ query: T }, Response.BadRequest<string>>
Validate the query string according to the given io-ts codec. Respond with 400 Bad Request
if the validation fails.
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.)
Parser.body<T>(codec: t.Type<T>): Middleware<{ body: T }, Response.BadRequest<string>>
Validate the request body according to the given io-ts codec. Respond with 400 Bad Request
if the validation fails.
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.
Parser.headers<T>(codec: t.Type<T>): Middleware<{ headers: T }, Response.BadRequest<string>>
Validate the request headers according to the given io-ts codec. Respond with 400 Bad Request
if the validation fails.
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.)
Parser.cookies<T>(codec: t.Type<T>): Middleware<{ cookies: T }, Response.BadRequest<string>>
Validate the request cookies according to the given io-ts codec. Respond with 400 Bad Request
if the validation fails.
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<{ body: 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:
Middleware.wrapNative<Result>(middleware, result?: (request: RequestBase) => Result): Middleware<Result, never>
Given Express middleware function middleware
, return the corresponding typera middleware.
If the result
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.
route.get(...): Route<Response>
route.post(...): Route<Response>
route.put(...): Route<Response>
route.delete(...): Route<Response>
route.head(...): Route<Response>
route.options(...): Route<Response>
route.patch(...): Route<Response>
route.all(...): Route<Response>
route(method, ...): Route<Response>
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 (middleware1, middleware2, ...
) 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 users can refer to the code: common, express, koa,
route.use(...middleware)
applyMiddleware(...middleware)
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.
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.
router(...routes: Route<any>[]): Router
Use the router()
function to create a router. Give it zero or more routes to add.
Router.add(...routes: Route<any>[]): Router
The .add()
method adds more routes to the router. Note that it returns a new Router
instance instead of modifying the existing one.
Router.handler()
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()))
Upgrading
Upgrading instructions covers upgrading to new major versions.
Releasing
$ yarn lerna version <major|minor|patch>
$ yarn lerna publish from-git
Open https://github.com/akheron/typera/releases, edit the draft release, select the newest version tag, adjust the description as needed.