Nswag: Proposal for a new, more efficient typescript fetch code generation

Created on 28 Mar 2020  路  10Comments  路  Source: RicoSuter/NSwag

I've noticed that for larger API surface areas the code generated can grow exponentially. One way around this would be to use TypeScript more effectively to enforce standard fetch conventions.

This can be accomplished by generating primarily types, which have 0 bytes of output along with some helper code; this means that essentially no matter how large your API is, the generated code is always a consistent size of a few kilobytes.

/** Routes */
type AddUserRoute    = "/api/users";
type DeleteUserRoute = "/api/users";
type AddPostRoute    = "/api/posts";

/** Models */

type AddPostModel = {
    subject?: string
    body: string
}

type AddPostReturnModel = {
    id: number
}

type AddUserModel = {
    name: string
    age: number
}

type AddUserReturnModel = {
    id: number
}

type DeleteUserModel = { 
    id: number
};

type TypedRequestInit<T = any> = T | ({ model: any } & RequestInit);

type TypedResponse<T = any> = T;

/** Constants */
const defaultRequestInfo : RequestInit = {
    headers: {
        "Content-Type": "application/json"
    }
}

/** Overloads */

/** Generated Add Post description goes here */
async function fetchPost(info: AddPostRoute, init: TypedRequestInit<AddPostModel>): Promise<AddPostReturnModel>;

/** Generated Add User description goes here */
async function fetchPost(info: AddUserRoute, init: TypedRequestInit<AddUserModel>): Promise<AddUserReturnModel>;

async function fetchPost(info: RequestInfo, init: TypedRequestInit) {
    const response = await fetch(
        info,
        "model" in init 
            ? { method: "POST", ...defaultRequestInfo, ...init, body: JSON.stringify(init.model) }
            : { method: "POST", ...defaultRequestInfo, body: JSON.stringify(init) }
    );

    return await response.json();
}

/** Overloads */
async function fetchDelete(info: DeleteUserRoute, init: TypedRequestInit<DeleteUserModel>): Promise<Response>;

async function fetchDelete(info: string, init: TypedRequestInit) {
    let modelInInit = "model" in init;
    let model       = modelInInit ? init.model : init;
    let keys        = Object.keys(init)
    let qs          = "?";

    for (let i = 0; i < keys.length; ++i) {
        let key   = keys[i];
        let value = model[key];

        if (i > 0)
            qs += "&";

        qs += encodeURIComponent(key) + "=" + encodeURIComponent(value);
    }

    return await fetch(
        info + qs,
        modelInInit
            ? { method: "DELETE", ...defaultRequestInfo, ...init }
            : { method: "DELETE", ...defaultRequestInfo }
    );
}

/** Example Usage */
fetchPost("/api/users", { name: "Ted", age: 25 });
fetchPost("/api/posts", { body: "This is a test" });
fetchDelete("/api/users", { id: 5 });

Most helpful comment

Update:

I just converted my own application for a real-world usecase, here's a before and after comparison

image

Most of the changes were very straighforward, for example:

image

All 10 comments

So I started looking into implementing it, and it seems really feasible, but I've had some issues with DotLiquid, at least the way it's being used in this application.

In order to generate 'overloads' as I've proposed above I would need to be able to filter the Operations array by method (post/get/etc), but liquid's "where" is not functional, either in this project or in DotLiquid or in the way this project is using DotLiquid (I'm not sure why), but https://shopify.github.io/liquid/filters/where/ does not work.

Any ideas?

There was an earlier attempt at this #1248.

That proposal looks awesome, but I don't think mutually exclusive. My proposal is not geared at reducing output of existing templates, so the output should be much smaller than even that proposal, because it's mainly generating types and overloads, not worried about breaking changes in the existing templates

OK, I've created some new templates, and I think we can really slim down the output quite easily. By a lot.

With the adafruit API (https://api.apis.guru/v2/specs/adafruit.com/2.0.0/swagger.json)

(1) Current NSwag typescript generator: 269kb (175kb minified) (100%)
(2) Slim with names typescript: 40kb (final output 9kb minified) (5%)
(3) Slim without names typescript: 38kb (final output 4kb minified) (2%)

Where (1) looks like this:

new MyAPI().myOperationName(5);

(2) looks like this:

MyAPI.myOperationName({ myArg: 5 });

and (3) looks like this:

MyAPI.get("/my/path", { myArg: 5 })

That is a big improvement but if the public API is changed I assume it will not be accepted into NSwag.

@gabbsmo It would have to be a separate template; so instead of asking for 'TypeScript' we could call it 'TypeScriptSlim' or something

With this difference you probably dropped a lot of features - is "Current NSwag typescript generator" generated with interfaces or classes?

@RicoSuter In my example, "Current NSwag typescript generator" uses interfaces not classes. So it's even worse than 175kb with classes. We lose some features, I'm sure, but we also gain a couple features, like the ability to customize FormData or get a valid route string using parameters.

So for example, I have an action that returns GEOJSON, and I dont actually want to parse the data, just generate a URL to it, I can do the following;

var url = StatisticsClient.route("/api/{partner}/geojson", { 
                    partner,
                    start: startDate,
                    end: endDate
                });

// ..display a map with `url`

and a valid route will be generated. If I then change the route in ASP.NET and re-generate the binding, it will produce a typechecking error at compile time if there are new required parameters or the path changes.

Update:

I just converted my own application for a real-world usecase, here's a before and after comparison

image

Most of the changes were very straighforward, for example:

image

Hey @RicoSuter and all - I just published my templates and some tests to the following repo:

https://github.com/wivuu/nswag-liquid-slim

The templates can be found here: https://github.com/wivuu/nswag-liquid-slim/tree/master/Templates

I've been using the templates in my app and so far they're easy to use and pretty reliable! I'm making the license MIT so please feel free to incorporate them however you want; or if you don't want to please let me know and I will probably create my own supplemental nuget package.

If anyone has any suggestions/improvements to reduce output or improve performance please feel free to submit PRs.

Was this page helpful?
0 / 5 - 0 ratings