logo
  • Guide
  • Config
  • Plugin
  • API
  • Examples
  • Community
  • Modern.js 2.x Docs
  • English
    • 简体中文
    • English
    • Start
      Introduction
      Quick Start
      Upgrading
      Glossary
      Tech Stack
      Core Concept
      Page Entry
      Build Engine
      Web Server
      Basic Features
      Routes
      Routing
      Config Routes
      Data Solution
      Data Fetching
      Data Writing
      Data Caching
      Rendering
      Rendering Mode Overview
      Server-Side Rendering
      Streaming Server-Side Rendering
      Rendering Cache
      Static Site Generation
      React Server Components (RSC)
      Render Preprocessing
      Styling
      Styling
      Use CSS Modules
      Using CSS-in-JS
      Using Tailwind CSS
      HTML Template
      Import Static Assets
      Import JSON Files
      Import SVG Assets
      Import Wasm Assets
      Debug
      Data Mocking
      Network Proxy
      Using Rsdoctor
      Using Storybook
      Testing
      Playwright
      Vitest
      Jest
      Cypress
      Path Alias
      Environment Variables
      Output Files
      Deploy Application
      Advanced Features
      Using Rspack
      Using BFF
      Basic Usage
      Runtime Framework
      Creating Extensible BFF Functions
      Extend BFF Server
      Extend Request SDK
      File Upload
      Cross-Project Invocation
      Optimize Page Performance
      Code Splitting
      Inline Static Assets
      Bundle Size Optimization
      React Compiler
      Improve Build Performance
      Browser Compatibility
      Low-Level Tools
      Source Code Build Mode
      Server Monitor
      Monitors
      Logs Events
      Metrics Events
      Internationalization
      Basic Concepts
      Quick Start
      Configuration
      Locale Detection
      Resource Loading
      Routing Integration
      API Reference
      Advanced Usage
      Best Practices
      Custom Web Server
      Topic Detail
      Module Federation
      Introduction
      Getting Started
      Application-Level Modules
      Server-Side Rendering
      Deployment
      Integrating Internationalization
      FAQ
      Dependencies FAQ
      CLI FAQ
      Build FAQ
      HMR FAQ
      Upgrade
      Overview
      Configuration Changes
      Entry Changes
      Custom Web Server Changes
      Tailwind Plugin Changes
      Other Important Changes
      📝 Edit this page
      Previous pageRuntime FrameworkNext pageExtend BFF Server

      #Creating Extensible BFF Functions

      The previous section showed how to export a simple BFF function in a file. In more complex scenarios, each BFF function may need to do independent type validation, pre-logic, etc.

      Therefore, Modern.js exposes Api, which supports creating BFF functions through this API. BFF functions created in this way can be easily extended with functionality.

      #Example

      Note
      • The Api function can only be used in TypeScript projects, not in pure JavaScript projects.
      • Operator functions (such as Get, Query, etc. below) depend on zod, which needs to be installed in the project first.
      pnpm add zod

      A BFF function created by the Api function consists of the following parts:

      • Api(), the function that defines the interface.
      • Get(path?: string), specifies the interface route.
      • Query(schema: T), Redirect(url: string), extends the interface, such as specifying interface input parameters.
      • Handler: (...args: any[]) => any | Promise<any>, the function that handles the request logic of the interface.

      The server can define the input parameters and types of the interface. Based on the types, the server will automatically perform type validation at runtime:

      api/lambda/user.ts
      import { Api, Post, Query, Data } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string().email(),
      });
      
      const DataSchema = z.object({
        phone: z.string(),
      });
      
      export const addUser = Api(
        Post('/user'),
        Query(UserSchema),
        Data(DataSchema),
        async ({ query, data }) => ({
          name: query.name,
          phone: data.phone,
        }),
      );
      Note

      When using the Api function, ensure that all code logic is placed inside the function. Operations such as console.log or using fs outside the function are not allowed.

      The browser side can also use the integrated call method with static type hints:

      routes/page.tsx
      import { addUser } from '@api/user';
      
      addUser({
        query: {
          name: 'modern.js',
          email: 'modern.js@example.com',
        },
        data: {
          phone: '12345',
        },
      });

      #Interface Route

      As shown in the example below, you can specify the route and HTTP Method through the Get function:

      api/lambda/user.ts
      import { Api, Get, Query, Data } from '@modern-js/plugin-bff/server';
      
      // Specify the interface route, Modern.js sets `bff.prefix` to `/api` by default,
      // so the interface route is `/api/user`, and the HTTP Method is GET.
      export const getHello = Api(
        Get('/hello'),
        Query(HelloSchema),
        async ({ query }) => query,
      );

      When the route is not specified, the interface route is defined according to the file convention. As shown in the example below, with the function writing method, there is a code path api/lambda/user.ts, which will register the corresponding interface /api/user.

      api/lambda/user.ts
      import { Api, Get, Query, Data } from '@modern-js/plugin-bff/server';
      
      // No interface route specified, according to file convention and function name, the interface is api/user, HTTP Method is get.
      export const get = Api(Query(UserSchema), async ({ query }) => query);
      Info

      Modern.js recommends defining interfaces based on file conventions to keep routes clear in the project. For specific rules, see Function Routes.

      In addition to the Get function, you can use the following functions to define HTTP interfaces:

      FunctionDescription
      Get(path?: string)Accept GET requests
      Post(path?: string)Accept POST requests
      Put(path?: string)Accept PUT requests
      Delete(path?: string)Accept DELETE requests
      Patch(path?: string)Accept PATCH requests
      Head(path?: string)Accept HEAD requests
      Options(path?: string)Accept OPTIONS requests

      #Request

      The following are request-related operators. Operators can be combined, but must comply with HTTP protocol. For example, GET requests cannot use the Data operator.

      #Query Parameters

      Using the Query function, you can define the type of query. After using the Query function, the query information can be obtained in the input parameters of the interface processing function, and the query field can be added to the input parameters of the frontend request function:

      api/lambda/user.ts
      // Server-side code
      import { Api, Query } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string().email(),
      });
      
      export const get = Api(Query(UserSchema), async ({ query }) => ({
        name: query.name,
      }));
      routes/page.tsx
      // Frontend code
      get({
        query: {
          name: 'modern.js',
          email: 'modern.js@example.com',
        },
      });

      #Query Parameter Type Conversion

      URL query parameters are strings by default. If you need numeric types, you need to use z.coerce.number() for type conversion:

      api/lambda/user.ts
      import { Api, Get, Query } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const QuerySchema = z.object({
        id: z.string(),
        page: z.coerce.number().min(1).max(100), // Use z.coerce.number() to convert string to number
        status: z.enum(['active', 'inactive']),
      });
      
      export const getUser = Api(
        Get('/user'),
        Query(QuerySchema),
        async ({ query }) => {
          return {
            id: query.id,
            page: query.page, // page is a number type
            status: query.status,
          };
        },
      );
      Note

      URL query parameters are all string types. If you need numeric types, you need to use z.coerce.number() for conversion, not z.number() directly.

      #Pass Data

      Using the Data function, you can define the type of data passed by the interface. After using Data, the interface data information can be obtained in the input parameters of the interface processing function.

      Caution

      If you use the Data function, you must follow the HTTP protocol. When the HTTP Method is GET or HEAD, the Data function cannot be used.

      api/lambda/user.ts
      import { Api, Data } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const DataSchema = z.object({
        name: z.string(),
        phone: z.string(),
      });
      
      export const post = Api(Data(DataSchema), async ({ data }) => ({
        name: data.name,
        phone: data.phone,
      }));
      routes/page.tsx
      // Frontend code
      post({
        data: {
          name: 'modern.js',
          phone: '12345',
        },
      });

      #Route Parameters

      Route parameters can implement dynamic routes and get parameters from the path. You can specify path parameters through Params<T>(schema: z.ZodType<T>)

      import { Api, Get, Params } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        id: z.string(),
      });
      
      export const queryUser = Api(
        Get('/user/:id'),
        Params(UserSchema),
        async ({ params }) => ({
          name: params.id,
        }),
      );

      #Request Headers

      You can define the request headers required by the interface through the Headers<T>(schema: z.ZodType<T>) function and pass the request headers through integrated calls:

      import { Api, Headers } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const headerSchema = z.object({
        token: z.string(),
      });
      
      export const queryUser = Api(Headers(headerSchema), async ({ headers }) => ({
        name: headers.token,
      }));

      #Parameter Validation

      As mentioned earlier, when using functions such as Query and Data to define interfaces, the server will automatically validate the data passed from the frontend based on the schema passed to these functions.

      When validation fails, you can catch errors through Try/Catch:

      try {
        const res = await postUser({
          query: {
            user: 'modern.js',
          },
          data: {
            message: 'hello',
          },
        });
        return res;
      } catch (error) {
        console.log(error.data.code); // VALIDATION_ERROR
        console.log(JSON.parse(error.data.message));
      }

      At the same time, you can get complete error information through error.data.message:

      [
        {
          code: 'invalid_string',
          message: "Invalid email",
          path: [0, 'user'],
          validation: "email"
        },
      ];

      #Middleware

      You can set function middleware through the Middleware operator. Function middleware will execute before validation and interface logic.

      Info

      The Middleware operator can be configured multiple times, and the execution order of middleware is from top to bottom

      import { Api, Query, Middleware } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string().email(),
      });
      
      export const get = Api(
        Query(UserSchema),
        Middleware(async (c, next) => {
          console.info(`access url: ${c.req.url}`);
          await next();
        }),
        async ({ query }) => ({
          name: query.name,
        }),
      );

      #Data Transformation Pipe

      The Pipe operator can pass in a function that executes after middleware and validation are completed. It can be used in the following scenarios:

      1. Transform query parameters or data carried by the request.
      2. Perform custom validation on request data. If validation fails, you can choose to throw an exception or directly return error information.
      3. If you only want to do validation without executing interface logic (for example, the frontend does not do separate validation, uses the interface for validation, but in some scenarios you don't want the interface logic to execute), you can terminate subsequent execution in this function.

      Pipe defines a transformation function. The input parameters of the transformation function are query, data, and headers carried by the interface request. The return value will be passed to the next Pipe function or interface processing function as input parameters, so the data structure of the return value generally needs to be the same as the input parameters.

      Info

      The Pipe operator can be configured multiple times. The execution order of functions is from top to bottom. The return value of the previous function is the input parameter of the next function.

      import { Api, Query, Pipe } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string(),
      });
      
      export const get = Api(
        Query(UserSchema),
        Pipe<{
          query: z.infer<typeof UserSchema>;
        }>(input => {
          const { query } = input;
          if (!query.email.includes('@')) {
            query.email = `${query.email}@example.com`;
          }
          return input;
        }),
        async ({ query }) => ({
          name: query.name,
        }),
      );

      Also,

      import { Api, Query, Pipe } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string().email(),
      });
      
      export const get = Api(
        Query(UserSchema),
        Pipe<{
          query: z.infer<typeof UserSchema>;
        }>((input, end) => {
          const { query } = input;
          const { name, email } = query;
          if (!email.startsWith(name)) {
            return end({
              message: 'email must start with name',
            });
          }
          return input;
        }),
        async ({ query }) => ({
          name: query.name,
        }),
      );

      If you need to do more custom operations on the response, you can pass a function to the end function. The input parameter of the function is Hono's Context (c), and you can operate on c.req and c.res:

      import { Api, Query, Pipe } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string().email(),
      });
      
      export const get = Api(
        Query(UserSchema),
        Pipe<{
          query: z.infer<typeof UserSchema>;
        }>((input, end) => {
          const { query } = input;
          const { name, email } = query;
          if (!email.startsWith(name)) {
            return end(c => {
              c.res.status = 400;
              c.res.body = {
                message: 'email must start with name',
              };
            });
          }
          return input;
        }),
        async ({ query }) => ({
          name: query.name,
        }),
      );

      #Response

      The following are response-related operators. Through response operators, you can process responses.

      #Status Code HttpCode

      You can specify the status code returned by the interface through the HttpCode(statusCode: number) function

      import { Api, Query, Data, HttpCode } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string().email(),
      });
      
      const DataSchema = z.object({
        phone: z.string(),
      });
      
      export const post = Api(
        Query(UserSchema),
        Data(DataSchema),
        HttpCode(202),
        async ({ query, data }) => {
          someTask({
            user: {
              ...query,
              ...data,
            },
          });
        },
      );

      #Response Headers SetHeaders

      Supports setting response headers through the SetHeaders(headers: Record<string, string>) function

      import { Api, Get, SetHeaders } from '@modern-js/plugin-bff/server';
      
      export default Api(
        Get('/hello'),
        SetHeaders({
          'x-log-id': 'xxx',
        }),
        async () => 'Hello World!',
      );

      #Redirect

      Supports redirecting the interface through Redirect(url: string):

      import { Api, Get, Redirect } from '@modern-js/plugin-bff/server';
      
      export default Api(
        Get('/hello'),
        Redirect('https://modernjs.dev/'),
        async () => 'Hello Modern.js!',
      );

      #Request Context

      As mentioned above, through operators, you can get query, data, params, etc. in the input parameters of the interface processing function. But sometimes we need to get more request context information. At this time, we can get it through useHonoContext:

      api/lambda/user.ts
      import { Api, Get, Query, useHonoContext } from '@modern-js/plugin-bff/server';
      import { z } from 'zod';
      
      const UserSchema = z.object({
        name: z.string().min(2).max(10),
        email: z.string().email(),
      });
      
      export const queryUser = Api(
        Get('/user'),
        Query(UserSchema),
        async ({ query }) => {
          const c = useHonoContext();
          const userAgent = c.req.header('user-agent');
          return {
            name: query.name,
            userAgent,
          };
        },
      );

      #FAQ

      #Can I use TypeScript instead of zod schema

      If you want to use TypeScript instead of zod schema, you can use ts-to-zod to convert TypeScript to zod schema first, and then use the converted schema.

      The reasons we chose zod instead of pure TypeScript to define input parameter type information are:

      • zod has a low learning curve.
      • In the validation scenario, zod schema has stronger expressiveness than TypeScript.
      • zod is easier to extend.
      • Solutions for obtaining TypeScript static type information at runtime are not mature enough.

      For specific comparisons of different solutions, you can refer to Why Use Zod. If you have more ideas and questions, please feel free to contact us.

      #More Practices

      #Add HTTP Cache to Interface

      In frontend development, some server interfaces (such as some configuration interfaces) have long response times, but actually don't need to be updated for a long time. For such interfaces, we can set HTTP cache to improve page performance:

      import { Api, SetHeaders } from '@modern-js/plugin-bff/server';
      
      export const get = Api(
        // Cache will only take effect when using integrated calls or fetch for requests
        // Within 1s, the cache does not validate and directly returns the response
        // Within 1s-60s, first return the old cache information, and at the same time re-initiate a validation request to fill the cache with new values
        SetHeaders({
          'Cache-Control': 'max-age=1, stale-while-revalidate=59',
        }),
        async () => {
          await wait(500);
          return 'Hello Modern.js';
        },
      );