Vite is a next-generation frontend build tool that leverages native ES modules during development and Rollup (or its Rust-based successor Rolldown) for production bundling. Unlike traditional bundlers that rebuild the entire dependency graph on every change, Vite serves source files directly over native ESM in development, transforming them on-demand. This architecture eliminates the cold-start penalty that plagues large projects using webpack or Parcel — the dev server starts in milliseconds regardless of project size, and hot module replacement (HMR) updates propagate in under 50ms.
For TypeScript frontend projects, Vite serves a dual purpose: it provides the fast development loop for building admin dashboards and monitoring UIs, and it produces optimized single-file bundles that can be embedded directly into Go binaries using go:embed. The production build inlines dynamic imports, tree-shakes unused code, and minifies CSS/JS into a compact payload — often under 200KB — that the Go server serves from memory without any filesystem dependency. This pattern eliminates deployment complexity: the entire application, including its web dashboard, is a single statically-linked binary.
This topic covers Vite’s architecture, configuration for single-file output, the Rolldown bundler integration, Vitest for testing, and the patterns for embedding Vite-built assets into Go server binaries.
Vite’s dev server exploits the browser’s native ES module support to skip bundling entirely during development. When you import a module, Vite intercepts the request, transforms the file (TypeScript compilation, JSX, CSS modules), and serves it directly. Dependencies from node_modules are pre-bundled once with esbuild (or Rolldown) into optimized ESM chunks. This two-tier approach — pre-bundle dependencies, serve application code on-demand — means the dev server starts instantly and HMR latency is independent of project size.
Rolldown is a Rust-based replacement for Rollup that Vite uses for production builds. It implements the Rollup plugin API while delivering 10-100x faster build times through native-speed parsing, tree-shaking, and code generation. For embedded dashboard builds where the output must be a single self-contained file, Rolldown’s inlineDynamicImports option collapses all dynamic import() calls into the main bundle, producing a single JS file with no chunk-splitting — exactly what’s needed for go:embed.
The standard pattern for embedding web UIs in Go binaries uses //go:embed to compile static assets into the binary at build time. This requires the frontend build to produce a small, deterministic set of output files — ideally one HTML file, one JS bundle, and one CSS file. Vite’s build configuration can be tuned to produce exactly this output: disable code splitting, inline dynamic imports, inline small assets as data URIs, and extract all CSS into a single file. The result is a deployment artifact that needs no web server, CDN, or filesystem — just a Go binary.
Vitest is a testing framework that runs on top of Vite’s transformation pipeline. Tests use the same config, plugins, and module resolution as the application — no separate Jest/Babel configuration to maintain. Because Vitest reuses Vite’s module graph, test startup is near-instant, and watch mode only re-runs tests affected by the changed file. For embedded dashboard projects, this means tests can import the same TypeScript, CSS modules, and environment variables the application uses without any test-specific stubbing or aliasing.
Vite’s plugin system extends the Rollup plugin interface with additional hooks for dev-server behavior (middleware, HMR handling, virtual modules). A single plugin can control both development and production behavior. Common plugins handle React/Preact JSX (@vitejs/plugin-react), SVG imports, environment variable injection, and legacy browser support. The plugin API’s simplicity — most plugins are under 100 lines — encourages project-specific build customizations rather than fighting configuration options.
A Vite project’s build behavior is controlled by vite.config.ts at the project root. During development, vite dev starts the native ESM dev server. For production, vite build invokes Rolldown to produce optimized output in the dist/ directory.
// vite.config.ts — configured for single-file output embeddable in Go
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
// Output to a directory that the Go build embeds.
outDir: 'dist',
// Produce a single JS file — no code splitting.
rollupOptions: {
output: {
// Inline all dynamic imports into the main bundle.
inlineDynamicImports: true,
// Deterministic file names (no content hashes)
// so go:embed paths are stable.
entryFileNames: 'app.js',
assetFileNames: '[name][extname]',
},
},
// Inline assets smaller than 100KB as data URIs
// to minimize the number of output files.
assetsInlineLimit: 100_000,
// Disable CSS code splitting — one styles.css file.
cssCodeSplit: false,
// Target modern browsers (ESM support required).
target: 'es2020',
},
// Base path for asset URLs when served from Go's embed handler.
base: '/dashboard/',
});
package dashboard
import (
"embed"
"io/fs"
"net/http"
)
//go:embed dist/*
var dashboardFS embed.FS
// Handler returns an http.Handler that serves the embedded dashboard.
// All assets are compiled into the Go binary — no external files needed.
func Handler() http.Handler {
// Strip the "dist" prefix so /dashboard/app.js maps to dist/app.js.
sub, _ := fs.Sub(dashboardFS, "dist")
return http.StripPrefix("/dashboard/", http.FileServer(http.FS(sub)))
}
// In main.go:
// mux.Handle("/dashboard/", dashboard.Handler())
// vite.config.ts — with Vitest configuration
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
// Use jsdom for DOM APIs in tests.
environment: 'jsdom',
// Global test utilities — no need to import describe/it/expect.
globals: true,
// Setup files run before each test file.
setupFiles: ['./src/test/setup.ts'],
// Coverage configuration.
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/test/**'],
},
},
});
// src/components/MetricsChart.test.tsx
import { render, screen } from '@testing-library/react';
import { MetricsChart } from './MetricsChart';
describe('MetricsChart', () => {
it('renders loading state when data is not available', () => {
render(<MetricsChart data={null} />);
expect(screen.getByText('Loading metrics...')).toBeInTheDocument();
});
it('displays chart with correct data points', () => {
const mockData = {
labels: ['10:00', '10:05', '10:10'],
values: [42, 58, 35],
unit: 'ms',
};
render(<MetricsChart data={mockData} />);
expect(screen.getByText('42 ms')).toBeInTheDocument();
expect(screen.getByText('58 ms')).toBeInTheDocument();
});
it('handles empty dataset gracefully', () => {
render(<MetricsChart data={{ labels: [], values: [], unit: 'ms' }} />);
expect(screen.getByText('No data available')).toBeInTheDocument();
});
});
// package.json — scripts for the embedded dashboard
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ci": "vitest run --coverage"
}
}
# Makefile — coordinating Go and Vite builds
.PHONY: build dashboard
# Build the dashboard first, then embed it in the Go binary.
dashboard:
cd ui && npm run build
build: dashboard
go build -o bin/server ./cmd/server
# Development: run Vite dev server and Go server concurrently.
dev:
cd ui && npm run dev &
air # Go live-reload with Air
As an example, a TypeScript frontend project can use Vite to build an admin dashboard that visualizes database cluster health, query performance metrics, and index build progress. During development, engineers run vite dev with proxy rules that forward API requests to a local database instance, getting instant feedback on UI changes without rebuilding the Go server. The Vite dev server’s HMR updates React components in-place, preserving component state — particularly valuable when debugging complex chart interactions or modal flows.
For production, the build pipeline runs vite build to produce a single app.js and styles.css, which are embedded into the Go binary. This means the server binary is fully self-contained: operators deploy a single file that includes the database engine, HTTP API, and admin dashboard. No separate nginx or CDN configuration is needed for the web UI. The embedded assets are served from memory with proper cache headers, and since they’re compiled into the binary, they’re always version-consistent with the API they call.
Vitest runs the dashboard’s test suite using the same Vite configuration and TypeScript paths as the application. Tests cover component rendering, API response handling, and chart data transformations. Because Vitest shares Vite’s module resolution, tests can import application modules without any path aliasing or module mapping — a @/components/MetricsChart import in a test resolves the same way it does in the app.
Embedded dashboards need to resolve API endpoints differently in development (proxied through Vite) versus production (same-origin, served by the Go binary). Use Vite’s environment variables to configure this at build time rather than runtime.
// src/api/client.ts — environment-aware API client
const API_BASE = import.meta.env.VITE_API_BASE || '';
export async function fetchMetrics(timeRange: string) {
const response = await fetch(`${API_BASE}/api/v1/metrics?range=${timeRange}`);
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json();
}
// In development, vite.config.ts proxies /api to the Go server:
// server: { proxy: { '/api': 'http://localhost:8080' } }
// In production, API_BASE is empty — requests go to same origin.
Keep the production bundle lean by only including development tools (React DevTools, mock service workers) in dev mode. Vite’s import.meta.env.DEV flag is statically replaced at build time, enabling dead-code elimination to strip dev-only imports entirely.
// src/main.tsx — conditional dev tool loading
import { createRoot } from 'react-dom/client';
import { App } from './App';
async function bootstrap() {
// This entire block is removed from production builds
// because import.meta.env.DEV is replaced with `false`
// and the bundler eliminates the dead branch.
if (import.meta.env.DEV) {
const { setupMockWorker } = await import('./mocks/browser');
await setupMockWorker();
console.log('[dev] Mock service worker started');
}
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
}
bootstrap();
Since embedded dashboards compile into Go binaries, every KB of frontend assets increases the binary size. Configure Vite’s build reporting to flag when bundles exceed a threshold, catching asset bloat before it reaches production.
// vite.config.ts — build size limits
export default defineConfig({
build: {
// Warn when a chunk exceeds 150KB (gzipped).
chunkSizeWarningLimit: 150,
// Report compressed sizes in build output.
reportCompressedSize: true,
// Enable minification for smallest possible output.
minify: 'esbuild',
},
});
By default, Vite splits code at dynamic import boundaries and shared chunks. When the output is embedded via go:embed, the Go code must know every filename in advance. If the build produces chunk-abc123.js files with content hashes, the embed pattern breaks. Always set inlineDynamicImports: true for embedded builds and use deterministic output filenames without hashes. If you need code splitting for a non-embedded deployment, use separate Vite configs for each target.
The Vite dev server proxies API requests to the Go backend, so frontend code uses relative URLs like /api/v1/metrics. In production, these same URLs must resolve correctly when the Go server serves both the embedded dashboard and the API. Problems arise when the dashboard is mounted at a sub-path (e.g., /dashboard/) but API calls use root-relative paths. Set Vite’s base config to the mount path and use a centralized API client that resolves URLs correctly in both environments.
If the Go build doesn’t depend on the frontend build, developers may compile the Go binary without rebuilding the dashboard, serving stale assets. Use a Makefile or build script that always runs vite build before go build. In CI, make the Go compilation step explicitly depend on the frontend build output directory. Some teams add a build timestamp or git hash to the dashboard’s HTML template to make staleness visible.
Vitest reads the test key from vite.config.ts, but some Vite plugins (particularly those that modify the dev server) can interfere with Vitest’s test environment. If you encounter plugin-related test failures, use Vitest’s deps.optimizer config to exclude problematic plugins from the test pipeline, or create a separate vitest.config.ts that extends the base Vite config with test-specific overrides.