DEV Community

Cover image for Vona Integrated SSR vs Zova Standalone SSR
Uncle Pushui
Uncle Pushui

Posted on

Vona Integrated SSR vs Zova Standalone SSR

When you first run Cabloy Basic, you will usually encounter two kinds of commands:

# Start the complete Cabloy service
npm run dev

# Start the Zova frontend SSR development service
npm run dev:zova:web
# or
npm run dev:zova:admin
Enter fullscreen mode Exit fullscreen mode

Both can open an SSR page, but they serve different goals:

  • npm run dev normally serves the project at http://localhost:7102.
  • npm run dev:zova:* normally serves the Web or Admin page at http://localhost:9000.

A newcomer does not need every SSR detail at the start. Remember this first: use 9000 to iterate on the frontend quickly; use 7102 to confirm that the project runs through its complete production-style path.

Vona Integrated SSR vs Zova Standalone SSR

Two ports, two working rhythms

7102: Vona integrated SSR

When you visit 7102, the request first reaches the Vona backend service. Vona identifies the SSR Site for the URL, loads the corresponding Zova SSR build output, and returns the final HTML response to the browser.

So 7102 represents the complete runtime path:

Browser
  → Vona backend service
  → Zova SSR build output renders the page
  → browser takes over the page
Enter fullscreen mode Exit fullscreen mode

This matches the core production-style model: Vona carries the HTTP request and SSR integration, while Zova renders the frontend page.

Use 7102 when you need to confirm that:

  • the Web or Admin path is correct;
  • Vona can find and load the right frontend build output;
  • backend APIs, SSR pages, and the final HTTP response work together;
  • the project runs correctly through its complete path before delivery.

9000: Zova standalone SSR

When you visit 9000, the browser enters the Zova frontend development service directly. It still server-renders the page, but its purpose is faster frontend work: edits to user code can hot-reload, and page, route, first render, and hydration issues are easier to inspect quickly.

Browser
  → Zova frontend development service
  → SSR-rendered page + user-code hot reload
Enter fullscreen mode Exit fullscreen mode

Use 9000 first while you are:

  • changing page layout or interactions;
  • editing components, routes, or frontend state;
  • checking the SSR first paint and the behavior after browser hydration;
  • shortening the edit-to-feedback loop with hot reload.

7102 and 9000 are the default Cabloy Basic development ports. Environment configuration can change the numbers, but not the responsibilities of the two entry points.


How a Cabloy project works as a fullstack system

Cabloy’s fullstack model is built around two simple principles.

1. Frontend build output participates directly in backend SSR

  • Zova owns frontend application source: pages, components, routes, and frontend state.
  • Zova’s frontend bundle and SSR-related output are loaded and used by the Vona SSR flow.
  • Server rendering and browser hydration therefore stay on one coordinated delivery path.

A complete page visit can be understood as:

Browser request
  → Vona receives the request and finds the matching site
  → Zova SSR build output renders the page and prepares initial state
  → Vona returns HTML
  → the browser hydrates and continues the page
Enter fullscreen mode Exit fullscreen mode

That is the basic meaning of integrated SSR: frontend SSR is not an isolated page prerender. It is one fullstack request completed together by Vona and Zova.

2. Type information flows in both directions

Cabloy uses a frontend-backend separation architecture. It does not simply place one shared types.ts file between the two sides. Instead, bidirectional contracts enable frontend and backend types to be generated and shared automatically.

  • Backend → Frontend: Vona emits Swagger / OpenAPI contracts; Zova uses them to generate SDKs, types, and schema helpers.
  • Frontend → Backend: Zova generates structural metadata and typing surfaces for routes, components, icons, renderers, and related resources; Vona tooling and type hints can consume them.

The next section uses one small example to make both directions concrete. For now, the key idea is enough: Vona owns backend entry and SSR integration, Zova owns the frontend application and rendering, and build output plus contracts let them collaborate.


Why do types need to synchronize in both directions?

One common fullstack problem is that the backend and frontend maintain two definitions that look alike: the backend changes a field, but the frontend forgets to update it; the frontend adds a renderable resource, but the backend cannot safely reference it.

Vona → Zova: when a backend API changes

When a Controller, DTO, validation rule, or entity field changes, Vona owns the business fact. Vona expresses it as Swagger / OpenAPI, then Zova generates the matching API, types, and schema helpers:

Vona API / DTO / validation
  → Swagger / OpenAPI
  → generated Zova API and types
  → frontend Model / page consumes them
Enter fullscreen mode Exit fullscreen mode

The existing summary/:id endpoint in training-student shows the whole process. It returns a student summary, including a level title, summary text, and description length.

1. Define the endpoint and response DTO on the backend

The Vona Controller declares the URL, parameter, and response DTO:

// vona/.../training-student/src/controller/student.ts
@Web.get('summary/:id', { summary: $locale('StudentSummary') })
@Api.body(v.optional(), v.object(DtoStudentSummary))
@Core.serializer()
async summary(
  @Arg.param('id', v.tableIdentity()) id: TableIdentity,
): Promise<DtoStudentSummary | undefined> {
  return await this.scope.service.student.summary(id);
}
Enter fullscreen mode Exit fullscreen mode

The response DTO declares the actual fields. If the backend adds summaryText, this DTO is the contract source:

// vona/.../training-student/src/dto/studentSummary.tsx
@Dto<IDtoOptionsStudentSummary>()
export class DtoStudentSummary extends $Dto.get(() => ModelStudent, {
  columns: ['id', 'name', 'mobile', 'level'],
}) {
  @Api.field(v.title($locale('LevelTitle')))
  levelTitle: string;

  @Api.field(v.title($locale('Summary')))
  summaryText: string;
}
Enter fullscreen mode Exit fullscreen mode

2. Regenerate the Zova API and types

First make sure Vona’s Swagger output contains the new field, then run:

npm run zova :openapi:generate training-student
Enter fullscreen mode Exit fullscreen mode

This refreshes the generated Zova API method, OpenAPI response type, and schema facade. Do not edit generated files directly; the next generation will overwrite them.

3. Consume the generated API in the frontend

After generation, the Zova API surface provides trainingStudent.summary(...) with its generated response type. A frontend Model can wrap it with a thin semantic method:

// zova/.../training-student/src/model/student.ts
summary(id: TableIdentity) {
  return this.$$modelResource.queryItem({
    id,
    action: 'summary',
    queryFn: async () => {
      const res = await this.scope.api.trainingStudent.summary({
        params: { id },
      });
      return res ?? null;
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

A page or table action only needs to call student.summary(id) to receive fields such as summaryText and levelTitle. The frontend does not need to handwrite a second StudentSummary interface: when the backend DTO changes, regenerate the consumer and keep using the updated type.

The complete flow is: Vona DTO → Swagger / OpenAPI → openapi:generate → Zova API → Model / page.

Zova → Vona: when a frontend resource changes

Some facts belong to the frontend. A custom form field, table cell, route, or icon is implemented by Zova. Vona needs a stable resource identity to reference it, but it does not execute the frontend component source itself.

The current training-student module provides a compact example: Vona defines what a Student Level means and which values are allowed; Zova implements the matching level selector and level badge.

Vona: what Level means, its allowed values, and the renderer key to use
  → Zova: implements the form field and table cell behind that key
  → build and synchronize the handoff
  → Vona can safely reference the refreshed frontend resource
Enter fullscreen mode Exit fullscreen mode

When a Zova resource like this changes, use the complete build and synchronization flow for the relevant flavor. For Admin, for example:

npm run build:zova:admin
npm run deps:vona
Enter fullscreen mode Exit fullscreen mode

The role of these two steps becomes concrete in the training-student level renderer. Zova first declares a stable renderer key and implements the actual form control:

// zova/.../training-student/src/component/formFieldLevel/controller.tsx

declare module 'zova-module-a-openapi' {
  export interface IResourceFormFieldRecord {
    'training-student:formFieldLevel'?: IResourceFormFieldLevelOptions;
  }
}

@Controller()
export class ControllerFormFieldLevel extends BeanControllerBase {
  protected render() {
    const { items = [], itemValue = 'value', itemTitle = 'title' } = this.$props.options ?? {};
    return (
      <div>
        {items.map(item => (
          <button key={String(item[itemValue])} type="button">
            {item[itemTitle]}
          </button>
        ))}
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

npm run build:zova:admin produces the Admin SSR and REST handoff artifacts. npm run deps:vona then synchronizes that handoff to Vona. After synchronization, Vona DTO or field metadata can reference the key and pass typed options:

// vona/.../training-student/src/entity/student.tsx
@Api.field(
  v.title($locale('Level')),
  ZovaRender.field('training-student:formFieldLevel', {
    items: studentLevelItems,
    placeholder: $locale('Level'),
  }),
  ZovaRender.cell('training-student:level', { items: studentLevelItems }),
  z.union([z.literal(1), z.literal(2), z.literal(3)]),
)
level: number;
Enter fullscreen mode Exit fullscreen mode

On the backend, DtoStudentSelectResItem inherits from ModelStudent and uses DTO field metadata to define how the list and form should be presented. After receiving the DTO, the frontend dynamically renders it from the renderer key and options: Zova still executes the actual JSX component, while the DTO only describes which renderer to use and which arguments to pass. It does not import or execute the frontend component source directly.

You do not need to memorize every command. The important direction is: the backend owns APIs and business rules; the frontend owns pages and renderers. When something changes, pass the contract from the side that owns the fact to the side that consumes it.


Where to explore next

If you are new to Cabloy, this is a useful learning sequence:

  1. Change a page through 9000 to experience Zova development and hot reload.
  2. Visit the same page through 7102 to see how Vona carries the complete SSR request.
  3. Change an API field and inspect how generated frontend types change from OpenAPI.
  4. Add a frontend renderer and learn why it needs a build and synchronization handoff to Vona.

As a project grows, this division makes problems easier to locate: is it a page-development issue, a Vona integration issue, or a contract-synchronization issue? You no longer have to describe every mismatch as simply “frontend and backend are out of sync.”

Further reading

Create fast feedback at 9000, then prove the complete runtime path at 7102.

Top comments (0)