Skip to content

Add a chatbot with Express + Vite

Goal: the same floating assistant as the Next.js guide, on a site with an Express backend and a Vite + React front end.

  • .env
  • vite.config.ts proxies /api to Express in development
  • Directorysrc
    • server.ts Express: /api/a2a (+ /config) and /api/nebulas-surfaces
    • main.tsx
    • App.tsx renders the chat
    • index.css Tailwind + tokens
  1. Install the dependencies.

    Terminal window
    npm install express dotenv react react-dom \
    @gnomondigital/nebulas-kit-core @gnomondigital/nebulas-kit-react
    npm install -D vite @vitejs/plugin-react tailwindcss@3 postcss autoprefixer \
    typescript @types/express @types/react @types/react-dom

    Then set up Tailwind as described in Installation.

  2. Write the Express server. Import from /express (or the package root) so Next.js is never loaded.

    src/server.ts
    import "dotenv/config";
    import express, { type Router } from "express";
    import { configureNebulasServer } from "@gnomondigital/nebulas-kit-core";
    import {
    createA2AExpressRouter,
    createNebulasSurfaceRouter,
    } from "@gnomondigital/nebulas-kit-core/express";
    const app = express();
    app.use(express.json({ limit: "2mb" })); // allows room for file uploads
    configureNebulasServer({
    a2aUrl: process.env.A2A_URL,
    nebulasApiUrl: process.env.NEBULAS_API_URL,
    nebulasApiKey: process.env.NEBULAS_API_KEY,
    nebulasProjectId: process.env.NEBULAS_PROJECT_ID,
    nebulasAuthStrategy: "resource_owner_password",
    authProvider: "auth0",
    auth0: {
    domain: process.env.AUTH0_DOMAIN ?? "",
    clientId: process.env.AUTH0_CLIENT_ID ?? "",
    clientSecret: process.env.AUTH0_CLIENT_SECRET ?? "",
    audience: process.env.AUTH0_A2A_AUDIENCE,
    },
    auth0ServiceUser: {
    username: process.env.AUTH0_ROP_USERNAME ?? "",
    password: process.env.AUTH0_ROP_PASSWORD ?? "",
    },
    });
    // Chat: POST /api/a2a and GET /api/a2a/config
    app.use(
    "/api/a2a",
    createA2AExpressRouter(express, {
    strategy: "resource_owner_password",
    apiKey: process.env.NEBULAS_API_KEY,
    }) as Router
    );
    // Optional: forms and other widgets (see the Contact form guide)
    app.use(
    "/api/nebulas-surfaces",
    createNebulasSurfaceRouter(express, {
    apiKey: process.env.NEBULAS_API_KEY,
    }) as Router
    );
    app.listen(3001, () => console.log("API on http://localhost:3001"));
  3. Proxy /api in development so the browser can call the API on the same origin:

    vite.config.ts
    import { defineConfig } from "vite";
    import react from "@vitejs/plugin-react";
    export default defineConfig({
    plugins: [react()],
    server: {
    port: 5173,
    proxy: { "/api": { target: "http://localhost:3001", changeOrigin: true } },
    },
    });
  4. Render the chat.

    src/App.tsx
    import { A2AChatFloating } from "@gnomondigital/nebulas-kit-react";
    export default function App() {
    return (
    <>
    <main className="container mx-auto max-w-2xl py-16 px-4">
    <h1 className="text-2xl font-semibold">Welcome</h1>
    </main>
    <A2AChatFloating
    a2aEndpoint="/api/a2a"
    configEndpoint="/api/a2a/config"
    panelTitle="Assistant"
    fabClassName="bg-primary text-primary-foreground hover:bg-primary/90"
    suggestedPrompts={["What can you help me with?"]}
    />
    </>
    );
    }
  5. Run both processes.

    Terminal window
    node --watch --experimental-strip-types src/server.ts & npx vite

    Open http://localhost:5173.

Build the front end with vite build. Have Express serve the output as static files, and send every request that isn’t /api/* to index.html:

src/server.ts
import path from "path";
app.use(express.static(path.join(__dirname, "client")));
app.get("*", (req, res, next) => {
if (req.path.startsWith("/api")) return next();
res.sendFile(path.join(__dirname, "client", "index.html"));
});

The React code then calls /api/... on the same origin, so you don’t need CORS.