Skip to content

Migrate from server-side rendering to Vue.js frontend#384

Closed
dambrisco wants to merge 6 commits intotrunkfrom
claude/evaluate-frontend-frameworks-3ldWt
Closed

Migrate from server-side rendering to Vue.js frontend#384
dambrisco wants to merge 6 commits intotrunkfrom
claude/evaluate-frontend-frameworks-3ldWt

Conversation

@dambrisco
Copy link
Contributor

This PR represents a major architectural shift from a server-side rendered application using Pug templates to a modern Vue.js single-page application (SPA) with a REST API backend.

Summary

The application has been refactored to separate concerns between frontend and backend. The server now provides REST API endpoints instead of rendering HTML, while a new Vue.js client application handles all UI rendering and user interactions.

Key Changes

Backend (src/)

  • Removed all Pug template files and server-side page rendering logic
  • Converted page handlers (series.ts, registration.ts, players.ts, profile.ts, teams.ts, roster.ts, divisions.ts, seasons.ts, playoffSeries.ts, roles.ts, admins.ts, admin_groups.ts, banned_players.ts, ips.ts, index.ts) to REST API endpoints
  • Created new API modules in src/api/:
    • series.ts, teams.ts, profiles.ts, registration.ts, admin.ts, players-admin.ts, seasons-admin.ts, divisions-admin.ts, me.ts, csrf.ts, rules.ts
  • Removed template type definitions and pug-tree dependency
  • Updated server.ts to remove template initialization and register new API routes

Frontend (client/)

  • Added complete Vue.js application with:
    • Router configuration with routes for all major views
    • Pinia stores for auth and season state management
    • Vue components for all pages (HomeView, RegistrationView, ProfileView, RosterView, etc.)
    • Admin management views for seasons, divisions, players, teams, series, admins, roles, etc.
    • API client utility for communicating with backend
    • Bulma CSS styling
  • Added build configuration (vite.config.ts, tsconfig.json, package.json)

Configuration

  • Updated root package.json to include client build in dev/build scripts
  • Updated Dockerfile to include client build step
  • Removed template configuration from config.ts
  • Updated tsconfig.json to exclude client directory

Implementation Details

  • The API maintains the same data structures and business logic, only the presentation layer has changed
  • Authentication and session management remain server-side
  • CSRF protection is now provided via API endpoint
  • The frontend uses modern Vue 3 composition API with TypeScript
  • Pinia is used for centralized state management (auth, seasons)

https://claude.ai/code/session_019yX1hLw84s21udY91pQypp

Comment on lines +263 to +265
app.get('*', (_req, res) => {
res.sendFile(path.join(__dirname, '..', 'client', 'dist', 'index.html'))
})

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 2 days ago

In general, the fix is to introduce a rate‑limiting middleware for routes that perform relatively expensive operations (here, serving the SPA HTML via sendFile) and apply that middleware to those routes. For an Express app, a common approach is to use a library such as express-rate-limit to define limits (e.g., max N requests per IP per unit time) and then attach that limiter either globally or to specific routes.

For this codebase, the least invasive fix that preserves existing behavior is:

  1. Import express-rate-limit at the top of src/server.ts.
  2. Define a limiter specifically for the SPA catch‑all route, with reasonable defaults (for instance, 100 requests per 15 minutes per IP).
  3. Apply this limiter as middleware to the app.get('*', ...) route, leaving the rest of the routing logic unchanged.

Concretely:

  • At the top of src/server.ts, add an import for express-rate-limit (using the default import to match the existing ES module style imports).
  • Near the SPA routing section (before line 262), create a spaRateLimiter using rateLimit({ windowMs: ..., max: ... }).
  • Change app.get('*', (_req, res) => { ... }) to app.get('*', spaRateLimiter, (_req, res) => { ... }).

This localizes rate‑limiting to the highlighted filesystem‑accessing route and does not affect other API endpoints.

Suggested changeset 2
src/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/server.ts b/src/server.ts
--- a/src/server.ts
+++ b/src/server.ts
@@ -12,6 +12,7 @@
 import passport from 'passport'
 import passportSteam from 'passport-steam'
 import pg from 'pg'
+import rateLimit from 'express-rate-limit'
 const pool = new pg.Pool({
   ...config.db,
   user: config.db.user || undefined,
@@ -261,7 +262,13 @@
 
 // Serve the Vue SPA for all non-API, non-asset routes
 app.use(express.static(path.join(__dirname, '..', 'client', 'dist')))
-app.get('*', (_req, res) => {
+
+const spaRateLimiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 minutes
+  max: 100, // limit each IP to 100 SPA index requests per windowMs
+})
+
+app.get('*', spaRateLimiter, (_req, res) => {
   res.sendFile(path.join(__dirname, '..', 'client', 'dist', 'index.html'))
 })
 
EOF
@@ -12,6 +12,7 @@
import passport from 'passport'
import passportSteam from 'passport-steam'
import pg from 'pg'
import rateLimit from 'express-rate-limit'
const pool = new pg.Pool({
...config.db,
user: config.db.user || undefined,
@@ -261,7 +262,13 @@

// Serve the Vue SPA for all non-API, non-asset routes
app.use(express.static(path.join(__dirname, '..', 'client', 'dist')))
app.get('*', (_req, res) => {

const spaRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 SPA index requests per windowMs
})

app.get('*', spaRateLimiter, (_req, res) => {
res.sendFile(path.join(__dirname, '..', 'client', 'dist', 'index.html'))
})

package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -39,7 +39,8 @@
     "pg-sql": "^1.1.0",
     "redirect-https": "^1.3.1",
     "shortid": "^2.2.17",
-    "swiss-pairing": "^1.4.3"
+    "swiss-pairing": "^1.4.3",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@eslint/js": "^10.0.0",
EOF
@@ -39,7 +39,8 @@
"pg-sql": "^1.1.0",
"redirect-https": "^1.3.1",
"shortid": "^2.2.17",
"swiss-pairing": "^1.4.3"
"swiss-pairing": "^1.4.3",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@eslint/js": "^10.0.0",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Implements the frontend framework migration from server-rendered Pug
templates to a Vue 3 SPA with Vite, following an incremental strategy
that keeps the existing Express/Pug stack functional.

Backend changes:
- Add 13 new API controllers under src/api/ exposing all data
  needed by the SPA (me, csrf, rules, profiles, teams, series,
  registration, admin, seasons-admin, divisions-admin, players-admin)
- Modify CSRF middleware to accept X-CSRF-Token request header
  alongside existing _csrf form body field
- Wire all new routes in src/server.ts
- Add SPA static serving + catch-all fallback route
- Update package.json with build:client and dev:client scripts
- Exclude client/ from server tsconfig.json compilation

Frontend (client/):
- Vue 3 + Vite + TypeScript scaffold
- Vue Router with all page routes (public, auth-required, admin)
- Pinia stores: auth (user session + CSRF token), season (active season)
- API client wrapper with automatic X-CSRF-Token header injection
- Bulma CSS via npm (replaces CDN), dynamic bulmaswatch theme switching
- 25+ view components covering all app features
- Admin CRUD views for seasons, divisions, players, teams, series,
  roles, admins, banned players, and IP address lookup

https://claude.ai/code/session_019yX1hLw84s21udY91pQypp
- Delete all src/pages/ controllers and src/templates/ Pug files
- Remove pug and pug-tree dependencies
- Remove TemplatesConfig from config types and config factory
- Add draftsheet CSV endpoint to players-admin API
- Update Dockerfile to build and copy client/dist
- Wire SPA static serving + catch-all in server.ts

https://claude.ai/code/session_019yX1hLw84s21udY91pQypp
@dambrisco dambrisco force-pushed the claude/evaluate-frontend-frameworks-3ldWt branch from 3c0407c to ccb5650 Compare February 16, 2026 22:05
Express 5 / path-to-regexp v8 no longer accepts the :param? optional
syntax. Split standings and matchups routes into two registrations each
(with and without :round) to restore compatibility.

https://claude.ai/code/session_019yX1hLw84s21udY91pQypp
@dambrisco dambrisco closed this Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments