A minimal React + .NET 10 Multi-Page Application (MPA) starter framework. Features server-side routing, type-safe API integration, and zero-config HMR.
npm install @rubichandrap/create-poyo-appwwwroot/
ā THE PROBLEM:
- ASP.NET MVC only serves static files from wwwroot/
- React source code lives in a separate client project
- You can't directly reference React components from Razor views
- You need to build React ā copy to wwwroot ā reference in views
- Manual process, breaks hot reload, painful developer experience
`
Traditional Workarounds:
1. Separate deployments - React SPA + .NET API (loses MPA benefits)
2. Manual copying - Build React, copy to wwwroot (tedious, error-prone)
3. Complex build scripts - Custom tooling (hard to maintain)
---
⨠How Poyo Solves It
Poyo provides a complete integration between React (Vite) and .NET MVC:
`
ā
THE SOLUTION:
1. React source code in poyo.client/ (separate project)
2. Vite compiles React ā wwwroot/generated/ (automatic)
3. Manifest generation maps hashed files ā Razor partials
4. Hot reload works in development (Vite dev server)
5. Production builds automatically update references
6. Zero manual intervention required!
`
Development Mode:
`
User ā .NET MVC ā Razor View ā Vite Dev Server (localhost:5173)
ā
React Hot Reload āØ
`
Production Mode:
`
npm run build
ā
Vite compiles React ā wwwroot/generated/index-[hash].js
ā
generate-manifest.js creates _ReactAssets.cshtml
ā
.NET MVC serves from wwwroot/ with correct hashed filenames ā
`
Key Features:
- š„ Hot Module Replacement - React changes reload instantly in dev
- š¦ Automatic Asset Management - Hashed filenames handled automatically
- š Server-Side Rendering - SEO-friendly, fast initial load
- šÆ Type-Safe Integration - TypeScript types from OpenAPI
- š Server Data Injection - Pass data to React without API calls
- š ļø Route Management - Sync routes between server and client
---
⨠Features
- š Multi-Page Architecture - Server-side routing with React hydration for SEO-friendly pages
- š Demo Authentication - Simple cookie-based auth example (replace with your own)
- š¦ Server Data Injection - Pass data from server to client without API calls
- ⨠Modern Stack - React 19, TypeScript, Tailwind CSS v4, TanStack Query
- šÆ Type-Safe APIs - Auto-generated TypeScript types from OpenAPI
- š ļø Route Management - CLI tools for managing routes between server and client
---
šļø Architecture
`
Poyo/
āāā Poyo.Server/ # .NET 10 Server
ā āāā Controllers/ # MVC + API Controllers
ā āāā Middleware/ # Auth, Error handling
ā āāā Models/ # DTOs
ā āāā Services/ # Business logic
ā āāā Views/ # Razor views
ā
āāā poyo.client/ # React Client
āāā src/
ā āāā pages/ # React pages
ā āāā hooks/ # Custom hooks (usePage, etc.)
ā āāā hooks-api/ # TanStack Query hooks
ā āāā services/ # API services
ā āāā providers/ # Context providers
āāā scripts/ # Code generation tools
`
---
š Quick Start
$3
- .NET 10 SDK
- Node.js 20+
$3
`bash
Create a new project
npx @rubichandrap/create-poyo-app MyApp
Navigate to project
cd MyApp
Install dependencies (React + .NET)
npm install
npm run restore
Run development servers
npm run dev
`
$3
- Username: demo
- Password: password
---
š Core Concepts
$3
Pass data from server to client without API calls - like Laravel Livewire!
Poyo allows you to inject server-side data directly into your React components, eliminating the need for initial API calls and enabling server-driven UI patterns.
Server (C#):
`csharp
[Authorize]
public IActionResult Dashboard()
{
// Prepare data on the server
var data = new
{
message = "Hello from server!",
timestamp = DateTime.UtcNow,
user = User.Identity?.Name,
notifications = GetUserNotifications(),
settings = GetUserSettings()
};
// Inject into ViewBag
ViewBag.ServerData = JsonSerializer.Serialize(data);
return View();
}
`
View (Razor):
`cshtml
@{
ViewBag.ServerData = JsonSerializer.Serialize(new {
message = "Data from view!",
userId = User.FindFirst("sub")?.Value
});
}
`
Client (TypeScript):
`typescript
interface DashboardData {
message: string;
timestamp: string;
user: string;
notifications: Notification[];
settings: UserSettings;
}
export default function DashboardPage() {
// Access server data immediately - no loading state needed!
// Validates that data is a non-null object
const data = usePage();
if (!data) return No data available;
return (
{data.message}
Server time: {data.timestamp}
User: {data.user}
{/ Data is already here - no spinner, no API call! /}
);
}
`
How it works:
1. Server renders Razor view with data in ViewBag.ServerData
2. _Layout.cshtml injects it as window.SERVER_DATA
3. React hydrates and usePage() reads from window.SERVER_DATA
4. Validation: usePage() ensures data is a valid object (returns null otherwise).
5. Zero API calls for initial page load!
Benefits:
- ā
Faster initial render - No loading spinners
- ā
SEO-friendly - Data is in HTML
- ā
Type-safe - TypeScript knows the shape
- ā
Server-driven - Like Livewire/Inertia.js
- ā
Secure - Data prepared server-side with auth context
Use Cases:
- User profile data
- Dashboard statistics
- Notification counts
- User preferences
- Any data needed on page load
$3
Routes are defined in routes.json and can now support Custom Controllers and Flexible SEO:
`json
{
"path": "/Dashboard",
"name": "Dashboard",
"files": {
"react": "src/pages/Dashboard/index.page.tsx",
"view": "Views/Dashboard/Index.cshtml"
},
"isPublic": false,
"controller": "DashboardController", // Optional: Use custom controller
"action": "Index", // Optional: Custom action
"seo": { // Optional: SEO Metadata
"title": "My Dashboard",
"description": "View your stats",
"meta": {
"og:image": "https://..."
},
"jsonld": {
"@type": "WebPage"
}
}
}
`
CLI Commands:
`bash
Basic Add
npm run route:add YourPage
Add with Custom Controller & Action
node scripts/manage-routes.js add /Admin --controller AdminController --action Index
Skip View Generation (if controller handles it)
node scripts/manage-routes.js add /API/Proxy --controller ApiController --action Proxy --no-view
`
$3
Poyo now supports a data-driven SEO system. You don't need to touch .cshtml files for metadata.
- Title/Description: Set in routes.json.
- Meta Tags: Dictionary in routes.json (supports OpenGraph).
- JSON-LD: Inject structured data scripts automatically.
All metadata is injected server-side into _Layout.cshtml before the React app even loads, ensuring perfect SEO.
$3
Poyo uses a hybrid approach to balance security and usability:
1. Web (Browser): HttpOnly Cookies
* Why? Protected against XSS (JavaScript can't read them). Browsers send them automatically.
* How? Server sets an AspNetCore.Cookies cookie on login.
2. Mobile (Native Apps): JWT (Bearer Token)
* Why? flexible for native HTTP clients where cookies are clumsy.
* How? Login API returns a token. Mobile apps send it in Authorization: Bearer .
3. Client UI: "UI Token"
* What? A non-sensitive flag/token stored in localStorage.
* Why? Instant UI updates. React knows to show "Logout" instead of "Login" immediately without waiting for a server roundtrip.
* Security: This is NOT used for access control. The Server validates the Cookie (or Bearer token). If the cookie is missing/invalid, the request fails even if the UI token exists.
`csharp
[GuestOnly] // Redirects authenticated users
public IActionResult Login() => View();
[Authorize] // Requires authentication
public IActionResult Dashboard() => View();
`
---
š ļø Tech Stack
$3
- .NET 10
- ASP.NET Core MVC
- Cookie Authentication
$3
- React 19
- TypeScript
- Vite (Rolldown)
- TanStack Query
- React Hook Form + Zod
- Tailwind CSS v4
- Axios
---
š Project Structure
$3
- routes.json - Route definitions
- Poyo.Server/Program.cs - Server configuration
- poyo.client/src/app.tsx - Client entry point
- poyo.client/src/hooks/use-page.ts - Server data hook
$3
- Poyo.Server/Controllers/ - MVC controllers
- Poyo.Server/Controllers/Api/ - API controllers
- Poyo.Server/Middleware/Auth/ - Auth attributes
- poyo.client/src/pages/ - React pages
- poyo.client/scripts/ - Code generation
---
šÆ What's Included
$3
- Cookie authentication
- Demo auth service (replace with your own)
- MVC routing
- Server data injection ([ServerData] attribute)
- Guest-only pages ([GuestOnly] attribute)
- Error handling
- JSend response wrapper
$3
- React 19 + TypeScript
- Form validation (React Hook Form + Zod)
- Data fetching (TanStack Query)
- Server data hook (usePage)
- Route management CLI
- Tailwind CSS v4
---
š§ Customization
$3
The framework includes hardcoded demo auth. Replace AuthService.cs with your own implementation:
`csharp
// Poyo.Server/Services/Auth/AuthService.cs
public class AuthService : IAuthService
{
// Replace with real authentication
// - ASP.NET Core Identity
// - JWT tokens
// - OAuth/OIDC
// - Your custom solution
}
`
$3
The framework doesn't include database access. Add your own:
`bash
Entity Framework Core
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Or Dapper
dotnet add package Dapper
`
$3
Update Tailwind configuration in poyo.client/src/index.css:
`css
@theme {
--font-sans: YourFont, system-ui, sans-serif;
/ Add your theme variables /
}
`
---
š¤ Code Generation
Poyo includes powerful code generation tools to keep your client and server in sync.
$3
Generates TypeScript types from OpenAPI specification
`bash
npm run generate:dtos
`
- Fetches OpenAPI spec from server
- Generates TypeScript types using openapi-typescript
- Outputs to src/schemas/dtos.generated.ts
- Requires: Server running + VITE_OPENAPI_URL in .env
$3
Generates Zod validation schemas from TypeScript DTOs
`bash
npm run generate:schemas
`
- Reads generated DTOs
- Creates Zod schemas using ts-to-zod
- Outputs to src/schemas/validations.generated.ts
- Use in forms with zodResolver
$3
Generates production asset manifest for server-side rendering
`bash
npm run generate:manifest
`
Why this is CRITICAL:
In development, Vite serves assets directly:
`html
`
In production, Vite builds assets with hashed filenames:
`
dist/generated/
āāā index-C2LBw7bc.css ā Hash changes every build!
āāā index-COWd0_qB.js ā Hash changes every build!
āāā vendor-SQrKxH4E.js ā Hash changes every build!
`
The Problem:
Your Razor views need to reference these files, but the filenames change with every build!
The Solution:
generate-manifest.js reads Vite's manifest and generates _ReactAssets.cshtml:
`cshtml
`
How it works:
1. npm run build compiles React app
2. Vite creates .vite/manifest.json with file mappings
3. generate-manifest.js reads manifest
4. Generates _ReactAssets.cshtml with correct hashed filenames
5. _Layout.cshtml includes this partial in production
6. Your app loads with correct assets!
What happens if you forget:
`
ā 404 errors - Assets not found
ā Old cached assets loaded
ā Broken production deployment
ā White screen of death
`
When it runs:
- ā
Automatically after npm run build (via postbuild script)
- ā
Manually with npm run generate:manifest
Files involved:
- Input: poyo.client/dist/.vite/manifest.json (Vite output)
- Output: Poyo.Server/Views/Shared/_ReactAssets.cshtml (Razor partial)
- Used by: Poyo.Server/Views/Shared/_Layout.cshtml (in production)
$3
Add new route:
`bash
npm run route:add User/Profile
OR (with flags)
npm run route:add -- /Register --guest
`
What this command does:
1. Updates routes.json: Adds entry mapping /User/Profile to the React page and Razor view.
2. Scaffolds React Page: Creates poyo.client/src/pages/User/Profile/index.page.tsx.
Optionally use --flat for src/pages/User/profile.page.tsx style.*
3. Scaffolds Razor View: Creates Poyo.Server/Views/User/Profile/Index.cshtml.
Sets up the #root div with data-page-name="User/Profile" for hydration.*
Remove route:
`bash
npm run route:remove User/Profile
`
* Safe Deletion: Prompts to optionally delete both the React page and MVC View (and empty folders).
Sync routes:
`bash
npm run route:sync
`
* Forward Sync: Checks for missing files and offers Rescaffold/Prune.
* Reverse Sync: Checks for "untracked" files (React pages not in routes.json) and offers to Add/Delete them.
---
š Scripts
$3
`bash
npm run dev # Start dev server
npm run build # Build for production
npm run generate # Generate DTOs + schemas
npm run generate:dtos # Generate TypeScript types from OpenAPI
npm run generate:schemas # Generate Zod schemas from DTOs
npm run generate:dtos # Generate TypeScript types from OpenAPI
npm run generate:schemas # Generate Zod schemas from DTOs
npm run route:add # Add new route (supports --controller, --action, --no-view)
npm run route:sync # Sync routes with files
npm run lint # Run linter
npm run format # Check formatting
`
$3
`bash
npm run dev # Start server (dotnet run)
npm run build # Build project (dotnet build)
npm run format # Check C# formatting
npm run format:fix # Fix C# formatting
npm run watch # Watch mode (dotnet watch)
npm run publish # Publish for production
``