Skip to content

TheoAnastasiadis/api-router-abstraction

Repository files navigation

Api Router Abstraction

coverage MIT License version

Typescript library to help abstract router details from controller implementations. Typesafe way to combine controllers with API URLs.

Imgur

Usage

Install

npm install api-router-abstraction

Define controllers

//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,
        }),
    },
}

Design your api interface structure

import { RouterGenerator } from "api-router-abstraction"

const generator = RouterGenerator.withConfig({
    controllerRegistry,
    bodyRegistry,
})

const { c, a, f } = generator.design()

Chain

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).

Alt

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).

Controller

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).

Compile

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
)

Use

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: {...}}

Available Validators

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.

Controller Implementations

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
   ...
}

Limitations

  • 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.

Testing

To test run npm run test.

Contribute 💓💗

If you like the principles behind this project and would like to cooperate contact me or open a PR!

Check out more

io-ts Runtime type system for IO decoding/encoding

Pissflix Civilized smartTV content platform for 2023

About

Typescript library for easy rest API prototyping

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published