Author avatar

Liam

30 Oct, 2018

blogpost banner

Make REST api to partial response, just like GraphQL

The story began in one day, I want to make NodeJS API endpoints like

GET /api/blogposts?fields=author{name,avatar},title,detail

and it should return me

[{
    "author": {
        "name": "Liam Ng",
        "avatar": "https://someurl",
    },
    "title": "blogpost title",
    "detail": "blogpost detail"
}]

When using GraphQL, partial response is a build in tool. However sometimes REST is a more suitable use case.

Searching the web, I do find some libraries that do similar things however the feature is somewhat limited. Developers should not be in situation that

  1. want to implement partial response
  2. not so supported in REST
  3. use GraphQL

Instead, REST should be able to achieve it as easily, with a library.

Things to acknowledge

  • Graphql still provide you a better way to do concurrent GET request (though the code might get complicated on retry on partial error)
  • It should be build with somethings like swagger to let user knows available fields (it do comes with handy function to return response schema in OpenAPI 3 format)

Introduce partial-responsify

tl;dr: you can find the code at github

Using it should be simple

import { PartialResponsify, ResponseFormat } from "partial-responsify";

// ideally should be inside DI
const pr = new PartialResponsify();

// it is normally inside ctx.query for Koa
// default don't support whitespace or - or _
// (normally should be camelcase?)
const fields = "name,coords,author{name{first}}";
const responseFormat: ResponseFormat = {
    fields: {
        author: {
            fields: {
                name: {
                    fields: {
                        first: {
                            type: "string",
                        },
                        last: {
                            type: "string",
                        },
                    },
                    type: "object",
                },
            },
            type: "object",
        },
        coords: {
            items: {
                items: {
                    type: "number",
                },
                type: "array",
            },
            type: "array",
        },
        license: {
            type: "string",
        },
        name: {
            type: "string",
        },
    },
    type: "object",
};

// you can use this fieldsToParse to track the fields usage
const fieldsToParse = pr.parseFields(fields, responseFormat);
console.log(fieldsToParse);

// and then you perform some logic and got the result
const result = {
    author: {
        name: {
            first: "Liam",
            last: "Ng",
        },
        url: "https://www.leliam.com",
    },
    coords: [[13.37, 1.337], [0, 0]],
    license: "MIT",
    name: "partial-responsify",
};
const res = pr.parseResult<any>(fieldsToParse, responseFormat, result);
console.log(res);
/*
the result should be:
[ [ [], 'name' ],
  [ [], 'coords' ],
  [ [ 'author', 'name' ], 'first' ] ]
{ author: { name: { first: 'Liam' } },
  coords: [ [ 13.37, 1.337 ], [ 0, 0 ] ],
  name: 'partial-responsify' }
*/

Since we use responseFormat to validate the response, it will be great if there is a function to generate swagger response schema based on it!

import { ResponseFormat, SchemaGenerator } from "partial-responsify";
const sgen = new SchemaGenerator();
const responseFormat: ResponseFormat = {
    items: {
        fields: {
            a: {
                type: "number",
            },
            b: {
                type: "string",
            },
            c: {
                type: "integer",
            },
            d: {
                format: "uuid",
                type: "string",
            },
            e: {
                fields: {
                    a: {
                        type: "number",
                    },
                },
                type: "object",
            },
        },
        type: "object",
    },
    type: "array",
};
const result = sgen.generate(responseFormat);
console.log(result);
/*
{
    items: {
        properties: {
            a: {
                type: "number",
            },
            b: {
                type: "string",
            },
            c: {
                type: "integer",
            },
            d: {
                format: "uuid",
                type: "string",
            },
            e: {
                properties: {
                    a: {
                        type: "number",
                    },
                },
                type: "object",
            },
        },
        type: "object",
    },
    type: "array",
}
*/

To make it better

It will require a library to auto generate Swagger JSON file to make it shine. It is not hard to make the library however will be large amount of works to support every edge case.