DEV Community

Cover image for [Nestia] I made backend simulator for frontend developers (similar with MSW, but fully automated)
Jeongho Nam
Jeongho Nam

Posted on • Edited on

[Nestia] I made backend simulator for frontend developers (similar with MSW, but fully automated)

Summary

Nestia Logo

I made NestJS backend server simulator in my library nestia.

You can build the backend server simulator by only one line command.

nestia will analyze your server codes, and generate imitation codes validating and composing mock-up data. It can be used in frontend application, without real backend server interaction.

  • Client (frontend) do not need to connect with backend server
  • Frontend can start development even if backend server is not ready
  • Backend do not need to waste time for mock-up data composition


const article: IBbsArticle = await api.functional.bbs.articles.store(
    {
        simulate: true, // activate simulator
        host: "http://127.0.0.1", // not important when simulating
    },
    "notice",
    {
        title: "Hello, world!",
        content: "This is a test article.",
    },
);


Enter fullscreen mode Exit fullscreen mode

Furthermore, nestia supports much more convenient and powerful features like below. With those features, you can get high productivity even gaining both high performance and easy development.

  • Productivity
    • Automatic e2e functions generator
    • NestJS simulator for frontend developers
    • SDK library for frontend developers
  • Performance
    • 25,000x faster validation
    • 200x faster JSON serialization
    • Totally 30x performance up
  • Easy Development
    • Only pure TypeScript type required
    • Besides, NestJS needs 3 times duplicated DTO definitions

p.s) Currently, frontend level backend server simulator is only possible, when the backend server be implemented by NestJS (+nestia). However, it would be possible in every backend server frameworks, soon.

Preface

In nowadays, I've developed a NestJS backend server simulator in my library nestia.

For reference, the simulator does not connect to remote backend server. It just imitates request data validation and response data construction, by itself. Therefore, frontend developers can start application development only with API interfaces, even if the backend server is not ready.

Below is a piece of SDK (Software Development Kit) code, generated by nestia, supporting the simulation mode. AS you can see, the simulator be activated just by configuring IConnection.simulate value to be true. Otherwise, the SDK would connect to the remote backend server.



/**
 * @packageDocumentation
 * @module api.functional.bbs.articles
 * @nestia Generated by Nestia - https://github.com/samchon/nestia 
 */
//================================================================
import { Fetcher } from "@nestia/fetcher";
import type { IConnection } from "@nestia/fetcher";
import typia from "typia";

import { NestiaSimulator } from "./../../../utils/NestiaSimulator";
import type { IBbsArticle } from "./../../../structures/IBbsArticle";

/**
 * Update an article.
 * 
 * @param section Section code
 * @param id Target article ID
 * @param input Content to update
 * @returns Updated content
 * 
 * @controller BbsArticlesController.update()
 * @path PUT /bbs/:section/articles/:id
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export async function update(
    connection: IConnection,
    section: string,
    id: string,
    input: update.Input,
): Promise<update.Output> {
    return !!connection.simulate
        ? update.simulate(
              connection,
              section,
              id,
              input,
          )
        : Fetcher.fetch(
              connection,
              update.ENCRYPTED,
              update.METHOD,
              update.path(section, id),
              input,
          );
}
export namespace update {
    export type Input = IBbsArticle.IStore;
    export type Output = IBbsArticle;

    export const METHOD = "PUT" as const;
    export const PATH: string = "/bbs/:section/articles/:id";
    export const ENCRYPTED: Fetcher.IEncrypted = {
        request: false,
        response: false,
    };

    export const path = (section: string, id: string): string => {
        return `/bbs/${encodeURIComponent(section ?? "null")}/articles/${encodeURIComponent(id ?? "null")}`;
    }
    export const random = (g?: Partial<typia.IRandomGenerator>): Output =>
        typia.random<Output>(g);
    export const simulate = async (
        connection: IConnection,
        section: string,
        id: string,
        input: update.Input,
    ): Promise<Output> => {
        const assert = NestiaSimulator.assert({
            method: METHOD,
            host: connection.host,
            path: path(section, id)
        });
        assert.param("section")("string")(() => typia.assert(section));
        assert.param("id")("uuid")(() => typia.assert(id));
        assert.body(() => typia.assert(input));
        return random(
            typeof connection.simulate === 'object'
            && connection.simulate !== null
                ? connection.simulate
                : undefined
        );
    }
}


Enter fullscreen mode Exit fullscreen mode

Within framework of backend developers, they also do not need to compose mock-up data, either.

Mock-up data composition would be automated by the simulator, and backend developers may focus on the business logic like API interface designs and main program developments.

Like below example code, just define only API interfaces. Then, builds simulator and delivers to frontend developers. Then you backend developer can implement application at the same time with frontend developers in parallel.

For reference, nestia also can generate e2e test functions automatically. Therefore, if you've succeeded to determine API specs, you can concentrate only on the main program development.



@Controller("bbs/articles")
export class BbsArticlesController {
    @TypedRoute.Post()
    public async store(
        @TypedBody() input: IBbsArticle.IStore,
    ): Promise<IBbsArticle> {
        return typia.random<IBbsArticle>();
    }
}


Enter fullscreen mode Exit fullscreen mode

Software Development Kit

SDK

In traditional software development, frontend developers recognize backend server API spec by reading Swagger Documents or similar one like RestDocs. By the way, as frontend developers are not robots but humans, reading and re-writing API specs in frontend application is a very annoying and error-prone work.

nestia also can generate Swagger Documents, and it is much evolved than traditional NestJS. However, I recommend NestJS backend developers to utilize SDK library much more, for frontend developers

In nestia case, it can generate SDK (Software Development Kit) library for frontend developers. As you can see from above example code, the SDK library is very simple and easy to use. Only import and function call statements are required. The SDK library will make frontend development much safer through type hints. NestJS backend server simulator also provided by the SDK library.

By the way, how nestia generates the SDK library? The secret is on your source code. nestia reads your NestJS backend server code directly (especially controllers), and analyzes which API routes are provided, and which DTO types are being used.

After these analyses, nestia writes fetch functios for each API routes, and import statemtns that are used in each controller classes. The SDK library is generated by such source code analyses and combininng fetch functions and import statements.

The NestJS backend server simulator, it is just a little extension of such SDK library.

AOT Compilation

By the way, in the previous #Summary section, I'd told that nestia is much easier than traditional NestJS. I'd told that when defining DTO schema, nestia needs only pure TypeScript type, but traditinal NestJS needs triple times duplicated definitions.

Looking at below example DTO schema definitions, you may understand what I am saying.



//----
// Traditional NestJS needs 3x duplicated definitions
//----
export class BbsArticle {
    @ApiProperty({
        type: () => AttachmentFile,
        nullable: true,
        isArray: true,
        description: "List of attached files.",
    })
    @Type(() => AttachmentFile)
    @IsArray()
    @IsOptional()
    @IsObject({ each: true })
    @ValidateNested({ each: true })
    files!: AttachmentFile[] | null;
}

//----
// Besides, nestia can understand pure TypeScript type
//----
export interface IBbsArticle {
    /**
     * List of attached files.
     */
    files: IAttachmentFile[] | null;
}

//----
// Therefore, advanced NestJS controller code can be
//----
@Controller("bbs/articles")
export class BbsArticlesController {
    @TypedRoute.Post()
    public async store(
        @TypedBody() input: IBbsArticle.IStore,
    ): Promise<IBbsArticle> {
        // just fine with pure interface
        //
        // validation is 25,000x times faster
        // JSON serialization is 200x faster
        ...
    }
}


Enter fullscreen mode Exit fullscreen mode

Looking at above example code, someone may ask me:

"Hey Samchon, how to validate the IBbsArticle.IStore type?

If a client requests with invalid data, is it possible to reject? As you know, TypeScript interface does not have any schema info in the runtime, and it is the reason why traditional NestJS has enforced developers to define triple times duplicated DTO schemas."

However, my answer is "It is enougly possible".

Also, even in this case, the secret is on your source code.

When you compile your NestJS backend servere, nestia will analyze your source codes, and analyzes how DTO schemas are being composed. Reading your DTO schemas and traveling properties of them, nestia will generate validation and JSON serialization codes for each API routes.

Looking at below compiled code, it is dedicatedly optimized for IBbsArticle.IStore type. It's the secret of nestia, which does not require triple times duplicated DTO schema definitions, but requires only pure TypeScript type. Also, such compile time optimization through source code analyzing is called, "AOT (Ahead of Time) compilation".



__param(2, core_1.default.TypedBody({ type: "assert", assert: input => {
        const __is = input => {
            const $is_custom = core_1.default.TypedBody.is_custom;
            const $is_url = core_1.default.TypedBody.is_url;
            const $io0 = input => "string" === typeof input.title && 3 <= input.title.length && 50 >= input.title.length && "string" === typeof input.body && (Array.isArray(input.files) && input.files.every(elem => "object" === typeof elem && null !== elem && $io1(elem)));
            const $io1 = input => (null === input.name || "string" === typeof input.name && 255 >= input.name.length && $is_custom("minLengt", "string", "1", input.name)) && (null === input.extension || "string" === typeof input.extension && 1 <= input.extension.length && 8 >= input.extension.length) && ("string" === typeof input.url && $is_url(input.url));
            return "object" === typeof input && null !== input && $io0(input);
        };
        if (false === __is(input))
            ((input, _path, _exceptionable = true) => {
                const $guard = core_1.default.TypedBody.guard;
                const $is_custom = core_1.default.TypedBody.is_custom;
                const $is_url = core_1.default.TypedBody.is_url;
                const $ao0 = (input, _path, _exceptionable = true) => ("string" === typeof input.title && (3 <= input.title.length || $guard(_exceptionable, {
                    path: _path + ".title",
                    expected: "string (@minLength 3)",
                    value: input.title
                })) && (50 >= input.title.length || $guard(_exceptionable, {
                    path: _path + ".title",
                    expected: "string (@maxLength 50)",
                    value: input.title
                })) || $guard(_exceptionable, {
                    path: _path + ".title",
                    expected: "string",
                    value: input.title
                })) && ("string" === typeof input.body || $guard(_exceptionable, {
                    path: _path + ".body",
                    expected: "string",
                    value: input.body
                })) && ((Array.isArray(input.files) || $guard(_exceptionable, {
                    path: _path + ".files",
                    expected: "Array<IAttachmentFile>",
                    value: input.files
                })) && input.files.every((elem, _index1) => ("object" === typeof elem && null !== elem || $guard(_exceptionable, {
                    path: _path + ".files[" + _index1 + "]",
                    expected: "IAttachmentFile",
                    value: elem
                })) && $ao1(elem, _path + ".files[" + _index1 + "]", true && _exceptionable) || $guard(_exceptionable, {
                    path: _path + ".files[" + _index1 + "]",
                    expected: "IAttachmentFile",
                    value: elem
                })) || $guard(_exceptionable, {
                    path: _path + ".files",
                    expected: "Array<IAttachmentFile>",
                    value: input.files
                }));
                const $ao1 = (input, _path, _exceptionable = true) => (null === input.name || "string" === typeof input.name && (255 >= input.name.length || $guard(_exceptionable, {
                    path: _path + ".name",
                    expected: "string (@maxLength 255)",
                    value: input.name
                })) && ($is_custom("minLengt", "string", "1", input.name) || $guard(_exceptionable, {
                    path: _path + ".name",
                    expected: "string (@minLengt 1)",
                    value: input.name
                })) || $guard(_exceptionable, {
                    path: _path + ".name",
                    expected: "(null | string)",
                    value: input.name
                })) && (null === input.extension || "string" === typeof input.extension && (1 <= input.extension.length || $guard(_exceptionable, {
                    path: _path + ".extension",
                    expected: "string (@minLength 1)",
                    value: input.extension
                })) && (8 >= input.extension.length || $guard(_exceptionable, {
                    path: _path + ".extension",
                    expected: "string (@maxLength 8)",
                    value: input.extension
                })) || $guard(_exceptionable, {
                    path: _path + ".extension",
                    expected: "(null | string)",
                    value: input.extension
                })) && ("string" === typeof input.url && ($is_url(input.url) || $guard(_exceptionable, {
                    path: _path + ".url",
                    expected: "string (@format url)",
                    value: input.url
                })) || $guard(_exceptionable, {
                    path: _path + ".url",
                    expected: "string",
                    value: input.url
                }));
                return ("object" === typeof input && null !== input || $guard(true, {
                    path: _path + "",
                    expected: "IBbsArticle.IStore",
                    value: input
                })) && $ao0(input, _path + "", true) || $guard(true, {
                    path: _path + "",
                    expected: "IBbsArticle.IStore",
                    value: input
                });
            })(input, "$input", true);
        return input;
    } })),


Enter fullscreen mode Exit fullscreen mode

For reference, such AOT compilation optimization is much light and faster than general logics accessing to each properties through for (const key in obj) statements like traditional NestJS. Measuring benchmark, it's 25,000 times faster.

About this secret (static access vs dynamic access), I'll write another article later. This is related to v8 engine optimization, and I can sure that it would be much interesting than any other stories.

Assert Benchmark

Random Generator

If you've read my article carefully, you may understand how to simulate response data, by yourself.

Yes, the response data also be composed by AOT compilation skill. Let's read the simulator (SDK) code again, then you can find that typia.random<T>() function be used. The typia.random<T>() is the last secret of nestia generated simulator. It generates random response data by analyzing the response DTO type.



/**
 * @packageDocumentation
 * @module api.functional.bbs.articles
 * @nestia Generated by Nestia - https://github.com/samchon/nestia 
 */
//================================================================
import { Fetcher } from "@nestia/fetcher";
import type { IConnection } from "@nestia/fetcher";
import typia from "typia";

import { NestiaSimulator } from "./../../../utils/NestiaSimulator";
import type { IBbsArticle } from "./../../../structures/IBbsArticle";

/**
 * Update an article.
 * 
 * @param section Section code
 * @param id Target article ID
 * @param input Content to update
 * @returns Updated content
 * 
 * @controller BbsArticlesController.update()
 * @path PUT /bbs/:section/articles/:id
 * @nestia Generated by Nestia - https://github.com/samchon/nestia
 */
export async function update(
    connection: IConnection,
    section: string,
    id: string,
    input: update.Input,
): Promise<update.Output> {
    return !!connection.simulate
        ? update.simulate(
              connection,
              section,
              id,
              input,
          )
        : Fetcher.fetch(
              connection,
              update.ENCRYPTED,
              update.METHOD,
              update.path(section, id),
              input,
          );
}
export namespace update {
    export type Input = IBbsArticle.IStore;
    export type Output = IBbsArticle;

    export const METHOD = "PUT" as const;
    export const PATH: string = "/bbs/:section/articles/:id";
    export const ENCRYPTED: Fetcher.IEncrypted = {
        request: false,
        response: false,
    };

    export const path = (section: string, id: string): string => {
        return `/bbs/${encodeURIComponent(section ?? "null")}/articles/${encodeURIComponent(id ?? "null")}`;
    }
    export const random = (g?: Partial<typia.IRandomGenerator>): Output =>
        typia.random<Output>(g);
    export const simulate = async (
        connection: IConnection,
        section: string,
        id: string,
        input: update.Input,
    ): Promise<Output> => {
        const assert = NestiaSimulator.assert({
            method: METHOD,
            host: connection.host,
            path: path(section, id)
        });
        assert.param("section")("string")(() => typia.assert(section));
        assert.param("id")("uuid")(() => typia.assert(id));
        assert.body(() => typia.assert(input));
        return random(
            typeof connection.simulate === 'object'
            && connection.simulate !== null
                ? connection.simulate
                : undefined
        );
    }
}


Enter fullscreen mode Exit fullscreen mode

When you compile the SDK library, such script would be written in the update.random() function, through AOT compilation.

The parameter g?: Partial<typia.IRandomGenerator> is a random seeder, but it is not neccessary.



export.update = update;
(function update() {
update.random = (g) => (generator => {
const $generator = typia_1.default.random.generator;
const $pick = typia_1.default.random.pick;
const $ro0 = (_recursive = false, _depth = 0) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2; return ({
id: (_d = (_c = (_b = ((_a = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _a !== void 0 ? _a : $generator.customs)) === null || _b === void 0 ? void 0 : _b.string) === null || _c === void 0 ? void 0 : _c.call(_b, [
{
name: "format",
value: "uuid"
}
])) !== null && _d !== void 0 ? _d : ((_e = generator === null || generator === void 0 ? void 0 : generator.uuid) !== null && _e !== void 0 ? _e : $generator.uuid)(),
section: (_j = (_h = (_g = ((_f = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _f !== void 0 ? _f : $generator.customs)) === null || _g === void 0 ? void 0 : _g.string) === null || _h === void 0 ? void 0 : _h.call(_g, [])) !== null && _j !== void 0 ? _j : ((_k = generator === null || generator === void 0 ? void 0 : generator.string) !== null && _k !== void 0 ? _k : $generator.string)(),
created_at: (_p = (_o = (_m = ((_l = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _l !== void 0 ? _l : $generator.customs)) === null || _m === void 0 ? void 0 : _m.string) === null || _o === void 0 ? void 0 : _o.call(_m, [
{
name: "format",
value: "date-time"
}
])) !== null && _p !== void 0 ? _p : ((_q = generator === null || generator === void 0 ? void 0 : generator.datetime) !== null && _q !== void 0 ? _q : $generator.datetime)(),
title: (_u = (_t = (_s = ((_r = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _r !== void 0 ? _r : $generator.customs)) === null || _s === void 0 ? void 0 : _s.string) === null || _t === void 0 ? void 0 : _t.call(_s, [
{
name: "minLength",
value: "3"
},
{
name: "maxLength",
value: "50"
}
])) !== null && _u !== void 0 ? _u : ((_v = generator === null || generator === void 0 ? void 0 : generator.string) !== null && _v !== void 0 ? _v : $generator.string)(((_w = generator === null || generator === void 0 ? void 0 : generator.integer) !== null && _w !== void 0 ? _w : $generator.integer)(3, 50)),
body: (_0 = (_z = (_y = ((_x = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _x !== void 0 ? _x : $generator.customs)) === null || _y === void 0 ? void 0 : _y.string) === null || _z === void 0 ? void 0 : _z.call(_y, [])) !== null && _0 !== void 0 ? _0 : ((_1 = generator === null || generator === void 0 ? void 0 : generator.string) !== null && _1 !== void 0 ? _1 : $generator.string)(),
files: ((_2 = generator === null || generator === void 0 ? void 0 : generator.array) !== null && _2 !== void 0 ? _2 : $generator.array)(() => $ro1(_recursive, _recursive ? 1 + _depth : _depth))
}); };
const $ro1 = (_recursive = false, _depth = 0) => { var _a, _b, _c, _d, _e; return ({
name: $pick([
() => null,
() => { var _a, _b, _c, _d, _e, _f; return (_d = (_c = (_b = ((_a = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _a !== void 0 ? _a : $generator.customs)) === null || _b === void 0 ? void 0 : _b.string) === null || _c === void 0 ? void 0 : _c.call(_b, [
{
name: "minLengt",
value: "1"
},
{
name: "maxLength",
value: "255"
}
])) !== null && _d !== void 0 ? _d : ((_e = generator === null || generator === void 0 ? void 0 : generator.string) !== null && _e !== void 0 ? _e : $generator.string)(((_f = generator === null || generator === void 0 ? void 0 : generator.integer) !== null && _f !== void 0 ? _f : $generator.integer)(5, 255)); }
])(),
extension: $pick([
() => null,
() => { var _a, _b, _c, _d, _e, _f; return (_d = (_c = (_b = ((_a = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _a !== void 0 ? _a : $generator.customs)) === null || _b === void 0 ? void 0 : _b.string) === null || _c === void 0 ? void 0 : _c.call(_b, [
{
name: "minLength",
value: "1"
},
{
name: "maxLength",
value: "8"
}
])) !== null && _d !== void 0 ? _d : ((_e = generator === null || generator === void 0 ? void 0 : generator.string) !== null && _e !== void 0 ? _e : $generator.string)(((_f = generator === null || generator === void 0 ? void 0 : generator.integer) !== null && _f !== void 0 ? _f : $generator.integer)(1, 8)); }
])(),
url: (_d = (_c = (_b = ((_a = generator === null || generator === void 0 ? void 0 : generator.customs) !== null && _a !== void 0 ? _a : $generator.customs)) === null || _b === void 0 ? void 0 : _b.string) === null || _c === void 0 ? void 0 : _c.call(_b, [
{
name: "format",
value: "url"
}
])) !== null && _d !== void 0 ? _d : ((_e = generator === null || generator === void 0 ? void 0 : generator.url) !== null && _e !== void 0 ? _e : $generator.url)()
}); };
return $ro0();
})(g);
})((update = exports.update) || (exports.update = {}));

Enter fullscreen mode Exit fullscreen mode




Closing

In current article, I've introduced NestJS backend server simulator through SDK library. However, as you know, frontend level backend server simulation is only possible when the backend server be developed with NestJS (+nestia).

By the way, I'm developing a new library @nestia/migrate, which can convert swagger.json file to a NestJS project. It's still in development, but most reached to the goal. After the @nestia/migrate library be released, you can simulate any backend server, even it's not developed with NestJS.

Look forward to it, application development would be much easier.

Top comments (3)

Collapse
 
deusexmachinaa profile image
Taehun

that's cool. how can I test this project on front-end? Is there a repo that I can check out right away?

Collapse
 
samchon profile image
Jeongho Nam

Until the demo to be prepared (maybe possible to start after 2 months later as busy in nowadays), just reference one of nestia test program please:

github.com/samchon/nestia/tree/mas...

Collapse
 
samchon profile image
Jeongho Nam

No demo front repo yet. Looking your comment, I think I should prepare it.