cohosted-frontend-backend

Installation
SKILL.md

Separate Development, Co-Hosted Deployment

Overview

The frontend and backend run in the same process (or the same container), with requests routed by URL prefix or route priority. Each side iterates independently during development and ships as a single artifact for deployment.

When to Use

  • The project needs a frontend UI but you don't want to maintain two deployment pipelines
  • Solo or small-team project where you want to simplify operations
  • You already have a backend API and need to add frontend pages under the same domain (avoiding CORS)
  • You need SSR for the first paint or a skeleton screen, then let the client-side SPA take over

Core Architecture Pattern

Browser Request
Single Server Process (Node.js or ASP.NET Core)
    ├─ /api/*              → Backend API routes (matched first)
    ├─ /graphql             → GraphQL endpoint (if any)
    ├─ /_next/* or static   → Frontend static assets (CSS/JS/images)
    ├─ /portal/* or /next/* → Frontend page routes
    └─ fallback             → Serve frontend index.html (SPA route catch-all)

Key principle: backend routes always take priority over the frontend fallback.

Decision Tree: Choosing an Integration Approach

Before implementation, choose the right approach for your project:

1. What is the backend framework?

Node.js (Fastify/Express/Egg.js/Koa)
  → Option A: Plugin integration (recommended)
  → Option B: Serve build output + 404 Fallback

ASP.NET Core
  → Option C: Static export + MapFallbackToFile (recommended)
  → Option D: Reverse proxy to a standalone Next.js process (complex scenarios)

Other (Go/Java/Python)
  → The approach from Option C is universally applicable

2. Does the frontend need SSR?

No (most scenarios)
  → Use Next.js output: 'export' for static export
  → Zero runtime dependencies; the backend only needs to serve static files

Yes — SSR for the first paint + client-side takeover
  → Node.js: Use @fastify/nextjs or Egg.js view templates
  → .NET: Standalone Next.js process + YARP reverse proxy

Server-rendered skeleton + SPA replacement
  → Use Pug/EJS/Razor to render an HTML skeleton
  → Inject the frontend build output's <script> and <link> tags into the HTML
  → After frontend loads, mount to #root and replace the skeleton content

3. Frontend routing strategy

basePath isolation (recommended starting point)
  → Frontend lives under /next, /portal, /app, etc.
  → No conflict with existing backend routes
  → Allows gradual migration; remove the prefix later

Root path takeover (end goal)
  → Frontend handles all unmatched routes
  → Backend API distinguished by /api/* prefix
  → Must ensure the fallback has the lowest priority

Implementation Steps (General Flow)

Step 1: Initialize the Frontend Project

Create a frontend subdirectory inside the backend project:

# Node.js project
project-root/
  ├── app.js (or server.js)
  ├── pages/          ← Next.js pages (if using plugin integration)
  └── client-app/     ← Standalone frontend project (if static export)

# ASP.NET Core project
src/Web/
  ├── Program.cs
  ├── Pages/          ← Razor Pages (keep as-is)
  ├── wwwroot/        ← Static assets root
  │   └── portal/     ← Frontend build output directory
  └── ClientApp/      ← Next.js source code

Frontend project initialization checklist:

  • Use the project's unified package manager (pnpm/yarn/npm)
  • Configure TypeScript
  • Configure Tailwind CSS or the project's existing styling solution
  • Set basePath to match the URL prefix agreed upon with the backend
  • If using static export, set output: 'export' and trailingSlash: true

Step 2: Configure Backend Routing

Principle: when adding frontend support, only add a fallback at the end of the routing pipeline — do not modify existing routes.

Node.js serving static files + fallback:

// Register static file middleware (pointing to the build output directory)
fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'dist'),
  prefix: '/portal/',
})

// Fallback unmatched routes to index.html
fastify.setNotFoundHandler((req, reply) => {
  if (req.url.startsWith('/portal')) {
    reply.sendFile('index.html', path.join(__dirname, 'dist'))
  }
})

ASP.NET Core serving static files + fallback:

app.UseStaticFiles(); // Already present, no changes needed

// Add fallback after all routes
app.MapFallback("/portal/{**slug}", async context => {
    context.Response.ContentType = "text/html";
    await context.Response.SendFileAsync(
        Path.Combine(app.Environment.WebRootPath, "portal", "index.html"));
});

Step 3: Update Build & Docker

Multi-stage Docker build template:

# ---- Frontend Build ----
FROM node:22-alpine AS frontend
WORKDIR /app
COPY path/to/ClientApp/ ./
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN pnpm install --frozen-lockfile && pnpm build

# ---- Backend Build ----
FROM <backend-base-image> AS backend
# ... existing backend build steps ...
# Copy frontend output to the static assets directory before publishing
COPY --from=frontend /app/out/ <static-assets-dir>/

# ---- Runtime ----
FROM <runtime-base-image>
# Final image has no Node.js — only the backend runtime + static files

Step 4: Configure the Development Experience

  • Frontend pnpm dev runs independently on port 3000
  • Backend dotnet run or node app.js runs on its own port
  • During frontend development, point to the backend API via environment variables (or configure a dev proxy)
  • In CI/CD, build the frontend first, then the backend

Step 5: Update .gitignore

# Frontend build output (generated during CI/Docker builds)
ClientApp/node_modules/
ClientApp/.next/
ClientApp/out/
wwwroot/portal/    # or the corresponding output directory

SSR Skeleton → SPA Takeover Pattern (Detailed)

This is a special pattern where the server first renders an HTML skeleton so users see content immediately, then the frontend JS takes over the entire page once loaded.

How it works:

1. Server renders HTML using a template engine (Pug/EJS/Razor)
2. HTML contains <div id="root">skeleton content</div>
3. Frontend build output's <script src="app.js"> is injected at the bottom
4. Browser:
   a. Immediately renders skeleton HTML (user sees the first paint; very fast FCP)
   b. Downloads and executes app.js
   c. React/Vue mounts to #root, replacing the skeleton content
   d. Client-side routing takes over; subsequent navigations don't hit the server

Difference from Next.js SSR hydration:

  • Hydration: the server renders the exact same React component tree as the client; the client "activates" event listeners without replacing the DOM
  • Skeleton replacement: the server renders simple placeholder HTML (may not contain React components); the client replaces it entirely

Skeleton replacement is simpler but can cause a flash; suitable for admin dashboards with low SEO requirements. Hydration has no flash but is more complex to implement.

Security Considerations

  • Frontend static assets must not contain sensitive information (API keys, database connection strings)
  • Only put public information in NEXT_PUBLIC_ prefixed environment variables
  • API authentication is handled by backend route middleware — do not rely on the frontend
  • If the backend has CSRF protection, ensure the frontend requests carry the correct token
  • Frontend basePath routes must not bypass the backend's authentication middleware
  • The static file directory must not expose .env, node_modules, or other sensitive content

Reference Implementations

The following are verified real-world implementations covering different tech stacks. Read the corresponding reference files when you need specific code examples:

  • references/node-fastify-nextjs.md — Fastify + Next.js plugin integration (Jeff-Tian/mp)
  • references/node-eggjs-umi.md — Egg.js + Umi/React skeleton replacement pattern (Jeff-Tian/alpha)
  • references/dotnet-nextjs-static-export.md — ASP.NET Core + Next.js static export (Jeff-Tian/Leg-Godt)
  • references/dotnet-nextjs-identity-server.md — ASP.NET Core IdentityServer + Next.js static export (A Duende based solution)

Common Pitfalls

  1. Route conflicts: The frontend fallback overrides API 404 responses. Fix: limit the fallback to a specific prefix or check the Accept header.
  2. Missing basePath: Frontend internal links don't use basePath, causing 404s for assets. Fix: Next.js's basePath config handles this automatically.
  3. Wrong Docker build order: Frontend output not copied to the backend's static directory before packaging. Fix: ensure COPY --from=frontend happens before publish in the Dockerfile.
  4. Dev environment inconsistency: Frontend dev server and backend port conflict. Fix: pin the frontend to port 3000, backend to a different port.
  5. Caching issues: Browser still uses old version after frontend update. Fix: Next.js uses content-hashed filenames by default; make sure you don't set an overly long Cache-Control.

Language Policy

  • Detect user language
  • Respond in same language
Related skills
Installs
4
GitHub Stars
1
First Seen
Apr 9, 2026