React/Vue SPA

Build and deploy single-page applications with Vite, React, or Vue.

Overview

Single-page applications (SPAs) built with React, Vue, or other frameworks typically use Vite for fast development and optimized production builds. This example shows how to build and deploy SPAs with ANDO.

Project Structure

my-spa/
├── src/
│   ├── App.tsx
│   ├── main.tsx
│   └── components/
├── public/
├── index.html
├── package.json
├── vite.config.ts
└── build.csando

Basic React/Vue Build

Build a Vite-based SPA:

var deploy = DefineProfile("deploy");

var frontend = Directory("./");

// Install dependencies
Node.Install();
Npm.Ci(frontend);

// Run linting and type checking
Npm.Run(frontend, "lint");
Npm.Run(frontend, "typecheck");

// Run tests
Npm.Test(frontend);

// Build for production
Npm.Build(frontend);

if (deploy)
{
    Cloudflare.EnsureAuthenticated();

    // Deploy to Cloudflare Pages
    Cloudflare.PagesDeploy(frontend / "dist", "my-spa");

    Log.Info("SPA deployed to Cloudflare Pages");
}

With Environment Variables

Configure environment-specific builds:

var deploy = DefineProfile("deploy");
var production = DefineProfile("production");

var frontend = Directory("./");

Node.Install();
Npm.Ci(frontend);

// Set environment for build
if (production)
{
    Environment.SetEnvironmentVariable("VITE_API_URL", "https://api.example.com");
    Environment.SetEnvironmentVariable("VITE_ENV", "production");
}
else
{
    Environment.SetEnvironmentVariable("VITE_API_URL", "https://api-staging.example.com");
    Environment.SetEnvironmentVariable("VITE_ENV", "staging");
}

// Build with environment variables
Npm.Build(frontend);

if (deploy)
{
    Cloudflare.EnsureAuthenticated();

    var projectName = production ? "my-spa" : "my-spa-staging";
    Cloudflare.PagesDeploy(frontend / "dist", projectName);

    Log.Info($"Deployed to {projectName}");
}

Usage:

# Build and deploy to staging
ando -p deploy

# Build and deploy to production
ando -p deploy,production

Deploy to Azure Static Web Apps

Deploy SPA to Azure Static Web Apps:

var deploy = DefineProfile("deploy");

var frontend = Directory("./");

Node.Install();
Npm.Ci(frontend);
Npm.Test(frontend);
Npm.Build(frontend);

if (deploy)
{
    Azure.EnsureAuthenticated();

    // Deploy to Azure Static Web Apps
    Azure.StaticWebAppsDeploy(
        frontend / "dist",
        "my-static-app",
        Env("AZURE_STATIC_WEB_APPS_API_TOKEN")
    );

    Log.Info("Deployed to Azure Static Web Apps");
}

Deploy to AWS S3 + CloudFront

Deploy SPA to S3 with CloudFront CDN:

var deploy = DefineProfile("deploy");

var frontend = Directory("./");

Node.Install();
Npm.Ci(frontend);
Npm.Test(frontend);
Npm.Build(frontend);

if (deploy)
{
    // Sync to S3
    Aws.S3Sync(frontend / "dist", "s3://my-spa-bucket");

    // Invalidate CloudFront cache
    Aws.CloudFrontInvalidate("E1234567890", "/*");

    Log.Info("Deployed to AWS S3 + CloudFront");
}

With Backend API

Build SPA alongside a .NET API:

var deploy = DefineProfile("deploy");

var api = Dotnet.Project("./api/Api.csproj");
var frontend = Directory("./frontend");

// Build API
Dotnet.SdkInstall();
Dotnet.Restore(api);
Dotnet.Build(api);
Dotnet.Test(Dotnet.Project("./api.tests/Api.Tests.csproj"));

// Build frontend
Node.Install();
Npm.Ci(frontend);
Npm.Test(frontend);
Npm.Build(frontend);

if (deploy)
{
    // Deploy API to Azure
    Azure.EnsureAuthenticated();
    Dotnet.Publish(api, o => o.Output(Root / "publish" / "api"));
    AppService.DeployZip("my-api", Root / "publish" / "api");

    // Deploy frontend to Cloudflare
    Cloudflare.EnsureAuthenticated();
    Cloudflare.PagesDeploy(frontend / "dist", "my-spa");

    Log.Info("Full stack deployed");
}

E2E Testing with Playwright

Run Playwright tests before deployment:

var deploy = DefineProfile("deploy");
var e2e = DefineProfile("e2e");

var frontend = Directory("./");

Node.Install();
Npm.Ci(frontend);
Npm.Test(frontend);
Npm.Build(frontend);

if (e2e || deploy)
{
    // Start preview server and run E2E tests
    Playwright.Test(frontend, o => o
        .WithProject("chromium")
        .WithBaseUrl("http://localhost:4173"));

    Log.Info("E2E tests passed");
}

if (deploy)
{
    Cloudflare.EnsureAuthenticated();
    Cloudflare.PagesDeploy(frontend / "dist", "my-spa");
    Log.Info("Deployed after E2E verification");
}

Monorepo with Turborepo/Nx

Build SPA in a monorepo setup:

var deploy = DefineProfile("deploy");

var monorepo = Directory("./");

Node.Install();
Npm.Ci(monorepo);

// Build all packages using Turborepo
Npm.Run(monorepo, "build");

// Or build specific app
Npm.Run(monorepo, "build", "--filter=web");

if (deploy)
{
    Cloudflare.EnsureAuthenticated();

    // Deploy the web app
    Cloudflare.PagesDeploy(monorepo / "apps" / "web" / "dist", "my-spa");

    Log.Info("Deployed web app from monorepo");
}

Docker Containerized SPA

Build SPA as a Docker container with nginx:

var deploy = DefineProfile("deploy");

var frontend = Directory("./");
var registry = "ghcr.io/myorg";
var version = Git.GetVersion();

// Build frontend
Node.Install();
Npm.Ci(frontend);
Npm.Test(frontend);
Npm.Build(frontend);

// Build Docker image (Dockerfile serves dist/ via nginx)
Docker.Build(frontend, $"{registry}/my-spa:{version}");

if (deploy)
{
    GitHub.EnsureAuthenticated();

    // Push to registry
    Docker.Push($"{registry}/my-spa:{version}");
    Docker.Push($"{registry}/my-spa:latest");

    Log.Info($"Pushed container image {version}");
}

Example Dockerfile:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

Key Operations

OperationPurpose
Npm.Ci()Install dependencies (CI mode)
Npm.Build()Build for production
Npm.Test()Run unit tests
Cloudflare.PagesDeploy()Deploy to Cloudflare
Playwright.Test()Run E2E tests

Tips

  • Use npm ci - Faster and more reliable than npm install in CI
  • Environment variables - Use VITE_* prefix for client-side env vars
  • Optimize bundle - Enable tree shaking and code splitting
  • Cache dependencies - Cache node_modules between builds if possible

See Also