Frontend integration

Vext includes a first-party frontend path for projects that want API routes, a browser app, development rebuilds, production builds, and static serving in one Vext project. The default scaffold uses React 19. Browser-side API helpers are imported from vextjs/frontend, while the backend runtime contract stays framework-independent.

The current release is the P0 frontend foundation. It supports one browser entry, plain CSS, static assets, HTML template injection, SPA fallback, and a Vext API client helper. It is not a complete application-level frontend framework yet, so it does not automatically scan src/client/pages/** for page routes, and it does not include nested layouts, loaders/actions, SSR, RSC, or Server Actions.

Table of Contents

1. When to use Vext Frontend

Use the default frontend integration when:

  • You want one Vext project to provide both API routes and browser pages.
  • You want vext dev to run the backend and frontend together.
  • You only need one React browser entry and can organize pages inside App.tsx.
  • You want vext build to output both server code and dist/client/ frontend assets.
  • You want vext start to serve static assets and SPA fallback in production.

Do not treat the current built-in path as the finished solution when:

  • You need file routing, nested routes, layouts, loaders/actions, or route-level splits.
  • You need SSR, React Server Components, or Server Actions.
  • You expect Sass, CSS Modules, Tailwind, or similar tools to be built into Vext by default.

Those capabilities belong to a later P1 application layer. Today you can add a userland router or styling tool yourself, but the official default path only promises the features documented here.

2. Create and run a full-stack project

Create the default full-stack React project:

npx vextjs create my-app
cd my-app
npm run dev

The default port is 3000. After the dev server is ready, open:

http://localhost:3000

The default page calls /api/hello from the browser. That API is defined in src/routes/index.ts, and the page itself lives in src/client/App.tsx.

If you created the project with --skip-install, run:

npm install
npm run dev

3. Understand the generated files

The default TypeScript full-stack project creates these frontend-related files:

my-app/
├── public/
│   └── favicon.svg
└── src/
    ├── client/
    │   ├── App.tsx
    │   ├── index.html
    │   ├── main.tsx
    │   └── styles.css
    ├── config/
    │   └── default.ts
    └── routes/
        └── index.ts
File or directoryHow to use it
src/client/main.tsxBrowser entry. Usually only mounts React and imports global styles.
src/client/App.tsxDefault page entry. Start here when changing the home page, organizing pages, or calling APIs.
src/client/styles.cssDefault global styles. You can split page or component CSS from it.
src/client/index.htmlHTML template for <title>, meta, #root, and Vext injection placeholders.
public/Public static assets, such as favicons, robots files, or images that should not be hashed.
src/routes/index.tsBackend API route example. It provides /api/hello and /api/health by default.
src/config/default.tsVext configuration. The default full-stack template includes a frontend block.

Do not edit files inside .vext/client/ or dist/client/ by hand. They are generated by Vext during dev/build.

4. Change the home page

The simplest home page entry is src/client/App.tsx. For example:

import { useEffect, useState } from "react";
import { createVextApiClient, isVextApiError } from "vextjs/frontend";

type HelloResponse = { message: string };

const api = createVextApiClient({
  schemaVersion: 1,
  kind: "client-contract",
  source: "routes-manifest",
  generatedAt: "template",
  routes: [
    {
      method: "GET",
      path: "/api/hello",
      operationId: "getApiHello",
      response: { type: "unknown" },
    },
  ],
  warnings: [],
} as const);

export function App() {
  const [message, setMessage] = useState("Loading...");
  const [error, setError] = useState("");

  useEffect(() => {
    api
      .GET("/api/hello")
      .then((data) => {
        setMessage((data as HelloResponse).message);
        setError("");
      })
      .catch((err) => {
        setMessage("Request failed");
        setError(isVextApiError(err) ? err.message : String(err));
      });
  }, []);

  return (
    <main className="shell">
      <section className="panel">
        <p className="eyebrow">Vext dashboard</p>
        <h1>{message}</h1>
        {error ? <p className="error">{error}</p> : <p>React client served by Vext.</p>}
      </section>
    </main>
  );
}

After saving the file, vext dev rebuilds the frontend for default src/client/** changes. Component-level HMR is not promised today; refresh the browser manually if needed.

5. Add page files

P0 does not automatically scan src/client/pages/** and turn those files into routes. You can use pages as a React page component directory, then import and render those components from App.tsx.

Create page files:

src/client/pages/Home.tsx
src/client/pages/About.tsx
// src/client/pages/Home.tsx
export function Home() {
  return (
    <section>
      <h1>Home</h1>
      <p>Welcome to the Vext frontend.</p>
    </section>
  );
}
// src/client/pages/About.tsx
export function About() {
  return (
    <section>
      <h1>About</h1>
      <p>This page is rendered by the React client.</p>
    </section>
  );
}

Wire them in App.tsx:

import { About } from "./pages/About";
import { Home } from "./pages/Home";

export function App() {
  const page = window.location.pathname === "/about" ? "about" : "home";
  return page === "about" ? <About /> : <Home />;
}

When a browser visits /about, Vext's SPA fallback returns the same index.html. The browser-side App.tsx decides which page to show. Creating src/client/pages/About.tsx alone does not create an automatic route in the current release.

If you need fuller client routing, you may add a userland router such as React Router. Vext does not install one by default.

6. Add components

Put reusable UI in src/client/components/:

src/client/components/Header.tsx
src/client/components/Button.tsx
// src/client/components/Header.tsx
export function Header() {
  return (
    <header className="header">
      <strong>Vext App</strong>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
    </header>
  );
}

Use it from a page:

import { Header } from "../components/Header";

export function Home() {
  return (
    <>
      <Header />
      <main>Home content</main>
    </>
  );
}

components is a recommended convention, not a framework-scanned directory. You can also organize by feature, such as src/client/features/users/ or src/client/features/orders/.

7. Add styles

The default scaffold uses plain CSS. Global styles are imported from main.tsx:

// src/client/main.tsx
import "./styles.css";

Page or component styles can live next to the component:

src/client/pages/Home.tsx
src/client/pages/Home.css
// src/client/pages/Home.tsx
import "./Home.css";

export function Home() {
  return <main className="home">Home</main>;
}

CSS is bundled by esbuild and emitted as a production CSS asset. Vext injects the generated CSS link into HTML.

The default P0 path only promises plain CSS. Sass, CSS Modules, Tailwind, and CSS-in-JS are not built into Vext by default. You can add them in your application, but the official guide does not document them as built-in support.

8. Add images and static assets

Vext recommends two asset locations:

LocationBest forHow to reference
public/Favicons, robots files, public images or files that should not be hashedUse URLs such as /favicon.svg or /logo.png
src/client/assets/Images, SVGs, and fonts imported by pages or CSSImport from TSX/CSS; esbuild emits hashed assets

public/ example:

public/logo.png
export function Header() {
  return <img src="/logo.png" alt="Logo" />;
}

src/client/assets/ example:

src/client/assets/hero.png
import heroUrl from "../assets/hero.png";

export function HomeHero() {
  return <img src={heroUrl} alt="Home hero" />;
}

If TypeScript reports missing image module types, add a declaration file in your app:

// src/client/assets.d.ts
declare module "*.png" {
  const src: string;
  export default src;
}

Do not edit generated output directories by hand: development output is .vext/client/, and production output is dist/client/.

9. Call Vext APIs

The default backend template provides:

GET /api/hello
GET /api/health

Use vextjs/frontend in browser code:

import { createVextApiClient, isVextApiError } from "vextjs/frontend";

type HelloResponse = { message: string };

const api = createVextApiClient({
  schemaVersion: 1,
  kind: "client-contract",
  source: "routes-manifest",
  generatedAt: "template",
  routes: [
    {
      method: "GET",
      path: "/api/hello",
      operationId: "getApiHello",
      response: { type: "unknown" },
    },
  ],
  warnings: [],
} as const);

try {
  const hello = (await api.GET("/api/hello")) as HelloResponse;
  console.log(hello.message);
} catch (err) {
  if (isVextApiError(err)) {
    console.error(err.status, err.message, err.details);
  } else {
    console.error(err);
  }
}

createVextApiClient() supports:

  • GET, POST, PUT, PATCH, and DELETE shortcut methods.
  • request(method, path, options) for other HTTP methods.
  • params replacement for path parameters such as /api/users/:id.
  • query, JSON body, request headers, signal, custom fetch, and baseUrl.
  • Non-2xx responses as VextApiError, checked with isVextApiError().
  • Automatic unwrap of Vext's { code: 0, data } response shape.

The template keeps a small handwritten contract in App.tsx so the example is self-contained. Builds also emit client-contract.json and api.generated.ts, but generated contracts currently keep request and response schema references as unknown; rich TypeScript inference from runtime schema definitions is not implemented yet.

10. Configure frontend

Minimal configuration

The default full-stack React template uses an object config. You can also enable defaults with:

import type { VextUserConfig } from "vextjs";

const config: VextUserConfig = {
  frontend: true,
};

export default config;

Common configuration

import type { VextUserConfig } from "vextjs";

const config: VextUserConfig = {
  port: 3000,
  adapter: "native",
  frontend: {
    enabled: true,
    framework: "react",
    entry: "src/client/main.tsx",
    indexHtml: "src/client/index.html",
    publicDir: "public",
    publicPath: "/",
    spaFallback: {
      enabled: true,
      exclude: ["/api/**", "/openapi.json", "/docs/**"],
    },
  },
};

export default config;

Configuration reference

FieldDefaultPurpose
frontendfalsetrue enables defaults; false disables frontend; object form configures fields.
frontend.enabledfalseEnables built-in frontend build and static serving.
frontend.framework"react"Frontend framework label. The current default scaffold is React.
frontend.root"src/client"Recorded frontend source directory; current compilation mainly uses entry and indexHtml.
frontend.entry"src/client/main.tsx"Browser entry file. Missing files fail fast.
frontend.indexHtml"src/client/index.html"HTML template. A minimal fallback shell is used when the file is missing.
frontend.outDirdev: .vext/client; prod: dist/clientFrontend output directory.
frontend.publicDir"public"Public static assets copied into output before bundling.
frontend.publicPath"/"URL prefix for generated asset links. It must be a path, not a full URL.
frontend.spaFallbacktrueReturns index.html for browser navigation paths that accept HTML.
frontend.spaFallback.exclude["/api/**", "/openapi.json", "/docs/**"]Paths that keep backend behavior.
frontend.apiClienttrueGenerates client-contract.json and api.generated.ts.
frontend.build.target"es2022"Browser build target.
frontend.build.minifyproduction trueMinifies frontend output.
frontend.build.sourcemapdevelopment trueEmits sourcemaps.
frontend.adapternoneReserved adapter metadata extension point; the current compiler does not call adapter build hooks.

For a sub-path deployment such as /app/:

export default {
  frontend: {
    enabled: true,
    publicPath: "/app/",
  },
};

publicPath changes generated script, style, and asset URLs.

11. HTML template

The default template is src/client/index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vext App</title>
    %VEXT_STYLES%
  </head>
  <body>
    <div id="root"></div>
    %VEXT_ENTRY%
  </body>
</html>

Current placeholders:

Placeholder or locationRendered output
%VEXT_STYLES%Replaced with generated CSS <link rel="stylesheet" ... data-vext-style> tags.
%VEXT_ENTRY%Replaced with the browser entry <script type="module" ... data-vext-entry></script>.
No %VEXT_STYLES%, but </head> existsStyle links are inserted before </head>.
No %VEXT_ENTRY%, but </body> existsThe entry script is inserted before </body>.
No </body>Generated tags are appended to the end of the file.
Missing indexHtml fileVext writes a minimal shell with <div id="root"></div>.

After vext build, rendered tags may look like:

<link rel="stylesheet" href="/assets/main-ABCD1234.css" data-vext-style>
<script type="module" src="/assets/main-EFGH5678.js" data-vext-entry></script>
Tip

A later P1 requirement plans to migrate official tokens to a %vext.*% style. Use %VEXT_STYLES% and %VEXT_ENTRY% in the current release.

12. Develop, build, and start

Develop:

npm run dev

Development frontend output is written to .vext/client/. Default src/client/** and public/** changes trigger a frontend rebuild instead of a backend cold restart. Backend API, config, route, service, middleware, plugin, locale, and preload changes keep their existing backend reload behavior.

Build:

npm run build

Production frontend output is written to dist/client/:

dist/client/
├── assets/
│   ├── main-<hash>.css
│   └── main-<hash>.js
├── api.generated.ts
├── client-contract.json
├── index.html
├── manifest.json
└── size-report.json

Start production:

npm start

vext start only serves existing production frontend output. When frontend is enabled but dist/client/index.html is missing, startup fails fast and tells you to run vext build first.

SPA fallback only handles GET / HEAD browser navigation requests that accept HTML. It excludes /api/**, /openapi.json, and /docs/** by default, so API and docs paths still reach the backend runtime.

13. Disable frontend for API-only projects

Create a new API-only project:

npx vextjs create my-api --template api --frontend none

Disable frontend in an existing project:

import type { VextUserConfig } from "vextjs";

const config: VextUserConfig = {
  frontend: false,
};

export default config;

Or:

export default {
  frontend: {
    enabled: false,
  },
};

When disabled, Vext does not build, watch, or serve src/client/** / public/** frontend assets.

14. Current boundaries and next stage

Supported today:

  • One browser entry: src/client/main.tsx.
  • React 19 default scaffold.
  • Plain CSS imports and CSS bundling.
  • Common image, font, and SVG asset imports.
  • public/ static asset copying.
  • HTML template injection with %VEXT_STYLES% / %VEXT_ENTRY%.
  • .vext/client/ development output and dist/client/ production output.
  • Static serving, cache headers, and SPA fallback.
  • vextjs/frontend API client helper.

Not supported yet:

  • Automatic page file routing.
  • Nested layouts, route groups, dynamic routes, and not-found routes.
  • Route loaders/actions, route-level splits, and prefetching.
  • Route-level head/meta/script/style management.
  • SSR, streaming, React Server Components, and Server Actions.
  • Built-in Sass, CSS Modules, or Tailwind.

The current workaround is to organize pages inside App.tsx, or add your own client router and styling tools. P1 should design application-level frontend routing, layouts, loaders/actions, type generation, splitting, and diagnostics as a separate layer.

15. Troubleshooting

SymptomWhat to do
Page changes do not appearConfirm npm run dev is running and the file is under default src/client/**; refresh the browser if needed.
/about returns the same HTMLThat is SPA fallback. Render by path inside App.tsx or your own router.
src/client/pages/About.tsx did not become a routeP0 does not scan pages. Import and render the file from App.tsx.
Image returns 404Use /logo.png for public/logo.png; import src/client/assets/logo.png from TSX/CSS.
Production start says frontend output is missingRun npm run build, then npm start.
API request receives HTMLSend Accept: application/json from API clients and add the API prefix to frontend.spaFallback.exclude.
Asset URLs miss a sub-pathSet frontend.publicPath, for example /app/.
publicPath config throwsUse a path such as /app/, not a full URL such as https://cdn.example.com/app/.
You want Sass, Tailwind, or CSS ModulesThey are not built in by default today; add them at the application level.
You want to disable frontendCreate with --template api --frontend none, or set frontend: false.

16. Maintainer reference

Regular users do not need these internals to use frontend integration. Maintainers can locate behavior through these sources of truth:

BehaviorSource of truth
Public browser helperssrc/frontend/index.ts
Frontend config resolutionsrc/frontend/tooling/config-resolver.ts
Client contract writingsrc/frontend/tooling/client-contract-writer.ts
esbuild build and HTML renderingsrc/frontend/tooling/client-build-compiler.ts
Static serving and SPA fallbacksrc/frontend/runtime/static-mount.ts
Dev frontend build integrationsrc/lib/dev/dev-bootstrap.ts
Dev file change classificationsrc/lib/dev/change-classifier.ts, src/lib/dev/file-watcher.ts
Production build integrationsrc/cli/build.ts
Production static mountsrc/lib/bootstrap.ts
Scaffold generationsrc/cli/create.ts

Next, review Configuration, Build, and CLI Commands.