Skip to content

Bootstrap a React App on CloudFront

This guide walks through starting a new React single-page application (SPA) and getting it live on Bymiljøetaten's CloudFront + S3 platform.

What you end up with

  • A React + TypeScript app scaffolded with Vite, running locally.
  • An S3 bucket and CloudFront distribution provisioned through Terraform, with HTTPS and DNS handled automatically.
  • A GitHub Actions workflow that builds the app and deploys it to S3 on every push.

Step 1: Scaffold the app

Vite sets up a working dev server and build pipeline with one command.

npm create vite@latest kattehotell -- --template react-ts
cd kattehotell
npm install
npm run dev

Replace kattehotell with your application's name.

npm run dev starts a local dev server with live reload. Open the printed URL in a browser to confirm the starter page loads before continuing.

Step 2: Commit the app to a new repository

Push the scaffolded app to a new GitHub repository under BYM-IKT. This is the application repository. It holds the React source code and stays separate from the Terraform Infrastruktur-repo you configure next.

Step 3: Provision the infrastructure

Follow Step 1: Provision the S3 bucket and CloudFront distribution and Step 2: Request pipeline access from Team Cloud in the S3 bucket + CloudFront guide.

Step 4: Add the deploy workflow

In your application repository, create .github/workflows/deploy-to-test.yml:

Where the workflow file goes
.
├── .github/
│   └── workflows/
│       └── deploy-to-test.yml
├── src/
│   ├── ...
│   └── main.tsx
├── ...
├── package.json
└── index.html

with the following content:

./.github/workflows/deploy-to-test.yml
name: Build and deploy website to TEST

on:
  push:
    branches: [main]

env:
  APP_URL: https://kattehotell.test.bymoslo.net # Replace with your application URL without quotes
  AWS_ACCOUNT_ID: "<<AWS_ACCOUNT_ID>>"
  S3_BUCKET_NAME: "<<S3_BUCKET_NAME>>"
  PROJECT_DIRECTORY: "."
  BUILD_PATH: "dist"

jobs:
  deploy-to-test:
    name: Build app and deploy to TEST
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    environment:
      name: testing
      url: ${{ env.APP_URL }}
    steps:
      - name: Check out repository
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

      - name: Setup up Node.js
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: 24

      - name: Clean install dependencies
        run: npm ci

      - name: Build
        env:
          CI: false
        run: npm run build

      - name: Upload to S3 and invalidate CloudFront cache
        uses: BYM-IKT/github-actions/upload-to-s3-and-invalidate-cloudfront@master
        with:
          aws-account-id:                      ${{ env.AWS_ACCOUNT_ID }}
          s3-bucket-name:                      ${{ env.S3_BUCKET_NAME }}
          build-directory:                     ${{ env.PROJECT_DIRECTORY }}/${{ env.BUILD_PATH }}
          cloudfront-distribution-domain-name: ${{ env.APP_URL }}

Replace <<AWS_ACCOUNT_ID>> with your AWS account ID and <<S3_BUCKET_NAME>> with the bucket name from Step 1 of the S3 bucket + CloudFront guide.

BUILD_PATH is set to dist because that's Vite's default build output directory. Confirm this matches build.outDir in vite.config.ts if you changed it.

Step 5: Push and verify

Push to main. The workflow builds the app and uploads it to S3, then invalidates the CloudFront cache.

  1. Open the Actions tab in the application repository and confirm the workflow run succeeds.
  2. Visit APP_URL from the workflow and confirm the app loads.

Common additions

BYM React apps commonly add these packages once the app is live:

Design system @oslokommune/punkt-react and @oslokommune/punkt-css provide the Oslo municipality's shared UI components.
Routing react-router for client-side navigation between pages.
Sign-in react-oidc-context and oidc-client-ts for authenticating users against OneLogin. If the app calls a protected API, see protecting API Gateway routes with a JWT authorizer for the matching backend setup.
HTTP client axios for calling backend APIs.
Data fetching @tanstack/react-query for caching and syncing server state.
Forms react-hook-form for managing form state and validation.
Validation zod for validating API responses and form input.