Typescript library to help abstract router details from controller implementations. Typesafe way to combine controllers with API URLs.
npm install api-router-abstraction
//set the arguments and (optional) return types for your controllers
controllerRegistry: {
getPostById: {
args: t.type({ //arguments are represented using io-ts types
id: t.number,
}),
},
getPostsByDate: {
args: t.type({
date: t.union([t.string, t.undefined]),
}),
returnType: t.type(...) //optional
},
createPost: {
args: t.type({
draft: t.boolean,
}),
body: "post", //this can be any key from the body registry
},
},
//body registry can help define large groups of arguments that won't appear in the URL path
bodyRegistry: {
post: {
fields: t.type({
author: t.string,
content: t.string,
}),
},
}
import { RouterGenerator } from "api-router-abstraction"
const generator = RouterGenerator.withConfig({
controllerRegistry,
bodyRegistry,
})
const { c, a, f } = generator.design()
Function c
or chain
will arrange validators in sequence. For example
c({ GET: c({ "/search": c({ "?q=string": f("search") }) }) })
will require that the request matches all 3 sequentially (ex. GET /search?q=cars
).
The function a
or alt
will arrange validators in parallel. For example
c({
POST: a(
{ "/posts": f("createPost") },
{ "/users": f("createUser") },
{ "/comments": f("createComment") }
),
})
will require that the request matches any of the arguments of a
(ex. POST /users
).
Function controller
or f
takes a key of controllerRegistry
as an argument. It is used to type-check your interface design and format url paths (see example below).
For your code to compile, all the controller nodes must be able to be supplied with all their arguments (as assigned in the controllerRegistry
object).
const generator = RouterGenerator.withConfig({
likePost: {
args: t.type({ postId: t.number }),
},
})
const router = generator.fromSchema(
c("/posts": c("/:postId(number)": f("likePost"))) //this compiles
)
//these don't!
const router = generator.fromSchema(
c("/posts": f("likePost"))
//too few arguments
)
const router = generator.fromSchema(
c("?isLogedIn=boolean":
c("/:postId(number)": f("likePost"))
) //too many arguments
)
The router
object comes with format and parse methods out of the box.
const router = generator.fromSchema(schema) //see image example above
router.parse({ path: "post/3", method: "GET" })
// controller: "getPostsById", consumed: {id: 3}}
router.format("createPost", {
draft: false,
body: { author: "John Doe", content: "Lorem ipsum..." },
})
// {path: "/posts?draft=false", method: "POST", body: {...}}
type | format examples | notes |
---|---|---|
Method | "GET" , "POST" , "PUT" , "DELETE" and even combinations like "GET, POST" etc. |
These will match the request method. These validators will be ignored if a method is not provided to the router.parse function. |
Path Param | "/pathName" or "/:paramName(string)" , "/:paramName(number)" , "/:paramName(boolean)" . |
They will match with the path URL string. The router.parse function will return a consumed object containing param names and values. For "/:paramName(number)" and "/:paramName(boolean)" if the provided path cannot be parsed according to the specified type, the validation won't resolve. "/:paramName(string)" will match everything. |
Query | "?name=string" , "?name1=number&name2=boolean" , "name1=string&name2=number!" , etc... |
They will match the query part of the request URL. If a ! is provided after the query param type, the matching won't resolve if the param is not present. |
body | "post_body" , "user_body" , etc... |
They will use io-ts parsers of the corresponding bodyRegistry key in order to validate the request body payload. |
You can enforce controller implementation types by providing the router as an argument of the ControllerImplementations
type.
import { ControllerImplementations } from "api-router-abstractions"
const controllerImpls: ControllerImplementations<typeof router> = {
getPostById: function (
args: { id: number },
router? //the router object
...rest: any
): unknown {...},
//if no returnType arg is provided in the controllerRegistry
getPostsByDate: function (
args: { date: string | undefined },
router?
...rest: any
): *ReturnType* {...},
//from the controllerRegistry
...
}
-
Authentication is not handled in this library. User objects should be provided in the rest parameters of controller functions.
-
Safety: this library should not be used in production without extensive security testing.
-
Unexpected behaviour: thorough testing on all possible URL configurations, charsets and lengths has not been carried out.
-
Adapters for server libraries (like express.js) must be written manually.
To test run npm run test
.
If you like the principles behind this project and would like to cooperate contact me or open a PR!
io-ts Runtime type system for IO decoding/encoding
Pissflix Civilized smartTV content platform for 2023