.NET React Templates

CLAUDE.md

Using CLAUDE.md and AGENTS.md files for AI-powered development with React .NET Templates

AI-Assisted Development

CLAUDE.md Files in Every Template

Each React .NET Template includes a comprehensive CLAUDE.md file designed to provide AI assistants like Claude Code, Cursor, and other AI coding tools with the complete context needed to effectively work with your project.

These files act as a living guide that helps AI assistants understand:

  • Project architecture and structure
  • Common development commands and workflows
  • Database migrations and ORM patterns
  • API development conventions
  • Testing strategies
  • Deployment configurations

Template-Specific CLAUDE.md Files

Each template has its own CLAUDE.md and AGENTS.md files tailored to its specific architecture:

These files are automatically included when you create a new project, ensuring AI assistants have immediate access to your project's context.

Why CLAUDE.md?

When working with AI coding assistants, context is everything. The better the AI understands your project structure, conventions, and patterns, the more accurate and helpful its suggestions become. CLAUDE.md files provide this context upfront, enabling:

  • Faster onboarding - AI assistants immediately understand your project structure
  • Better code generation - AI generates code that follows your conventions
  • Accurate suggestions - AI recommends the right tools and patterns for your stack
  • Consistent patterns - AI maintains architectural consistency across features

What's Inside CLAUDE.md

Below is the complete CLAUDE.md from the react-static template, which serves as a comprehensive reference for AI-assisted development:


CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) and other Agents when working with code in this repository.

Project Overview

A full-stack .NET 10 + React 19 + Next.js template combining ServiceStack backend with Next.js static site generation. Uses ASP.NET Core Identity for auth, OrmLite for application data, and Entity Framework Core for Identity data management.

Common Commands

Development

# Start both .NET and Vite dev servers (from project root)
dotnet watch

# After making changes to C# DTOs restart .NET before regenerating TypeScript DTOs by running:
cd MyApp.Client && npm run dtos

Building

# Build frontend (TypeScript + Vite)
cd MyApp.Client && npm run build

# Build backend (.NET)
dotnet build

# Build Tailwind CSS for Razor Pages
cd MyApp && npm run ui:build

Testing

# Frontend tests (Vitest)
cd MyApp.Client && npm run test        # Watch mode
cd MyApp.Client && npm run test:ui     # UI mode
cd MyApp.Client && npm run test:run    # Single run

# Backend tests (NUnit)
dotnet test

Database Migrations

# Run all migrations (both EF Core and OrmLite)
cd MyApp && npm run migrate

# Entity Framework migrations (for changes to Identity tables)
dotnet ef migrations add MigrationName
dotnet ef database update

# Revert last migration
cd MyApp && npm run revert:last

# Drop and re-run last migration (useful during development)
cd MyApp && npm run rerun:last

AutoQuery CRUD Development (using okai)

# Create new AutoQuery CRUD feature with TypeScript data model
npx okai init Table

# Regenerate C# AutoQuery APIs and DB migration from .d.ts model
npx okai Table.d.ts

# Remove AutoQuery feature and all generated code
npx okai rm Table.d.ts

Architecture

Hybrid Development/Production Model

Development Mode:

  • dotnet watch from MyApp starts .NET (port 5001) and Next.js dev server (port 3000), accesible via https://localhost:5001
  • ASP.NET Core proxies requests to Next.js dev server via NodeProxy (configured in Program.cs)
  • Hot Module Replacement (HMR) enabled via WebSocket proxying using MapNotFoundToNode, MapNextHmr, RunNodeProcess, MapFallbackToNode in Program.cs

Production Mode:

  • Next.js builds React app to MyApp.Client/dist/, which is copied to MyApp/wwwroot/ when published
  • ASP.NET Core serves static files directly from wwwroot - no Node.js required
  • Fallback to index.html for client-side routing

Modular Startup Configuration

AppHost uses .NET's IHostingStartup pattern to split configuration across multiple files in MyApp/:

This pattern keeps Program.cs clean and separates concerns. Each Configure.*.cs file is auto-registered via [assembly: HostingStartup] attribute.

Project Structure

MyApp/                         # .NET Backend (hosts both .NET and Next.js)
├── Configure.*.cs             # Modular startup configuration
├── Migrations/                # EF Core Identity migrations + OrmLite app migrations
├── Pages/                     # Identity Auth Razor Pages
└── wwwroot/                   # Production static files (from MyApp.Client/dist)

MyApp.Client/                  # React Frontend
├── app/                       # Next.js App Router pages
├── components/                # React components
├── lib/                       # Utilities and helpers
│   ├── dtos.ts                # Auto-generated from C# (via `npm run dtos`)
│   ├── gateway.ts             # ServiceStack JsonServiceClient
│   └── utils.ts               # Utility functions
├── public/                    # Static assets
├── dist/                      # Build output (production)
├── styles/                    # Tailwind CSS styles
└── next.config.mjs            # Next.js config for dev mode

MyApp.ServiceModel/            # DTOs & API contracts
├── *.cs                       # C# Request/Response DTOs
├── api.d.ts                   # TypeScript data models Schema
└── *.d.ts                     # TypeScript data models for okai code generation

MyApp.ServiceInterface/        # Service implementations
├── Data/                      # EF Core DbContext and Identity models
└── *Services.cs               # ServiceStack service implementations

MyApp.Tests/                   # .NET tests (NUnit)
├── IntegrationTest.cs         # API integration tests
└── MigrationTasks.cs          # Migration task runner

config/
└── deploy.yml                 # Kamal deployment settings
.github/
└── workflows/
    ├── build.yml              # CI build and test
    ├── build-container.yml    # Container image build
    └── release.yml            # Production deployment with Kamal

Database Architecture

Dual ORM Strategy:

  • OrmLite: All application data (faster, simpler, typed POCO ORM)
  • Entity Framework Core: ASP.NET Core Identity tables only (Users, Roles, etc.)

Both use the same SQLite database by default (App_Data/app.db). Connection string in appsettings.json.

Migration Files:

  • MyApp/Migrations/20240301000000_CreateIdentitySchema.cs - EF Core migration for Identity
  • MyApp/Migrations/Migration1000.cs - OrmLite migration for app tables (e.g., Booking)

Run npm run migrate to execute both.

Authentication Flow

  1. ASP.NET Core Identity handles user registration/login via Razor Pages at /Identity/* routes
  2. ServiceStack AuthFeature integrates with Identity via IdentityAuth.For<ApplicationUser>() in Configure.Auth.cs
  3. Custom claims added via AdditionalUserClaimsPrincipalFactory and CustomUserSession
  4. ServiceStack services use [ValidateIsAuthenticated] and [ValidateHasRole] attributes for authorization (see Bookings.cs)

ServiceStack .NET APIs

ServiceStack APIs adopt a DTOs-first approach utilizing message-based APIs. To create ServiceStack APIs create all related DTOs used in the API (aka Service Contracts) into a single file in the MyApp.ServiceModel project, e.g:

//MyApp.ServiceModel/Bookings.cs

public class GetBooking : IGet, IReturn<GetBookingResponse>
{
    [ValidateGreaterThan(0)]
    public int Id { get; set; }
}
public class GetBookingResponse
{
    public Booking? Result { get; set; }
    public ResponseStatus? ResponseStatus { get; set; }
}

The response type of an API should be specified in the IReturn<Response> marker interface. APIs which don't return a response should implement IReturnVoid instead.

By convention, APIs return single results in a T? Result property, APIs returns multiple results of the same type in a List<T> Results property. Otherwise APIs returning results of different types should use intuitive property names in a flat structured Response DTO for simplicity.

These C# Server DTOs are used to generate the TypeScript dtos.ts.

Validating APIs

Any API Errors are automatically populated in the ResponseStatus property, inc. Declarative Validation Attributes like [ValidateGreaterThan] and [ValidateNotEmpty] which validate APIs and return any error responses in ResponseStatus.

Protecting APIs

The Type Validation Attributes below should be used to protect APIs:

  • [ValidateIsAuthenticated] - Only Authenticated Users
  • [ValidateIsAdmin] - Only Admin Users
  • [ValidateHasRole] - Only Authenticated Users assigned with the specified role
  • [ValidateApiKey] - Only Users with a valid API Key
//MyApp.ServiceModel/Bookings.cs
[ValidateHasRole("Employee")]
public class CreateBooking : ICreateDb<Booking>, IReturn<IdResponse>
{
   //...
}

Primary HTTP Method

APIs have a primary HTTP Method which if not specified uses HTTP POST. Use IGet, IPost, IPut, IPatch or IDelete to change the HTTP Verb except for AutoQuery APIs which have implied verbs for each CRUD operation.

API Implementations

ServiceStack API implementations should be added to MyApp.ServiceInterface/:

//MyApp.ServiceInterface/BookingServices.cs
public class BookingServices(IAutoQueryDb autoquery) : Service
{
    public object Any(GetBooking request)
    {
        return new GetBookingResponse {
            Result = base.Db.SingleById<Booking>(request.Id)
                ?? throw HttpError.NotFound("Booking does not exist")
        };
    }

    // Example of overriding an AutoQuery API with a custom implementation
    public async Task<object> Any(QueryBookings request)
    {
        using var db = autoQuery.GetDb(request, base.Request);
        var q = autoQuery.CreateQuery(request, base.Request, db);
        return await autoQuery.ExecuteAsync(request, q, base.Request, db);
    }
}

APIs can be implemented with sync or async methods using Any or its primary HTTP Method e.g. Get, Post. The return type of an API implementation does not change behavior however returning object is recommended so its clear the Request DTO IReturn<Response> interface defines the APIs Response type and Service Contract.

The ServiceStack Service base class has convenience properties like Db to resolve an Open IDbConnection for that API and base.Request to resolve the IRequest context. All other dependencies required by the API should use constructor injection in a Primary Constructor.

A ServiceStack API typically returns the Response DTO defined in its Request DTO IReturn<Response> or an Error but can also return any raw custom Return Type like string, byte[], Stream, IStreamWriter, HttpResult and HttpError.

AutoQuery CRUD Pattern

ServiceStack's AutoQuery generates full CRUD APIs from declarative request DTOs. Example in Bookings.cs:

  • QueryBookings : QueryDb<Booking> → GET /api/QueryBookings with filtering/sorting/paging
  • CreateBooking : ICreateDb<Booking> → POST /api/CreateBooking
  • UpdateBooking : IPatchDb<Booking> → PATCH /api/UpdateBooking
  • DeleteBooking : IDeleteDb<Booking> → DELETE /api/DeleteBooking

No service implementation required - AutoQuery handles it. Audit fields (CreatedBy, ModifiedBy, etc.) auto-populated via [AutoApply(Behavior.AuditCreate)] attributes.

AutoQuery CRUD Docs

TypeScript DTO Generation

After changing C# DTOs in MyApp.ServiceModel/, restart the .NET Server then run:

cd MyApp.Client && npm run dtos

This calls ServiceStack's /types/typescript endpoint and updates dtos.ts with type-safe client DTOs. The Vite dev server auto-reloads.

okai AutoQuery Code Generation

The npx okai tool generates C# AutoQuery APIs and migrations from TypeScript data models (.d.ts files):

  1. TypeScript data model (MyApp.ServiceModel/Bookings.d.ts) defines the entity with decorators
  2. C# AutoQuery APIs (MyApp.ServiceModel/Bookings.cs) - auto-generated CRUD request/response DTOs
  3. C# OrmLite migration (MyApp/Migrations/Migration1000.cs) - auto-generated schema creation

This enables rapid prototyping: edit the .d.ts model, run npx okai Bookings.d.ts, then npm run migrate.

Important: The .d.ts files use special decorators (e.g., @validateHasRole, @autoIncrement) that map to C# attributes and .NET Types. The valid schema for these is defined in api.d.ts. Reference Bookings.d.ts for examples.

AutoQuery APIs

C# AutoQuery APIs allow creating queryable C# APIs for RDBMS Tables with just a Request DTO definition, e.g:

public class QueryBookings : QueryDb<Booking>
{
    public int? Id { get; set; }
    public decimal? MinCost { get; set; }
    public List<decimal>? CostBetween { get; set; }
    public List<int>? Ids { get; set; }
}

It uses these conventions to determine the behavior of each property filter:

ImplicitConventions = new() {
    {"%Above%",      "{Field} >  {Value}"},
    {"Begin%",       "{Field} >  {Value}"},
    {"%Beyond%",     "{Field} >  {Value}"},
    {"%Over%",       "{Field} >  {Value}"},
    {"%OlderThan",   "{Field} >  {Value}"},
    {"%After%",      "{Field} >  {Value}"},
    {"OnOrAfter%",   "{Field} >= {Value}"},
    {"%From%",       "{Field} >= {Value}"},
    {"Since%",       "{Field} >= {Value}"},
    {"Start%",       "{Field} >= {Value}"},
    {"%Higher%",     "{Field} >= {Value}"},
    {"Min%",         "{Field} >= {Value}"},
    {"Minimum%",     "{Field} >= {Value}"},
    {"Behind%",      "{Field} <  {Value}"},
    {"%Below%",      "{Field} <  {Value}"},
    {"%Under%",      "{Field} <  {Value}"},
    {"%Lower%",      "{Field} <  {Value}"},
    {"%Before%",     "{Field} <  {Value}"},
    {"%YoungerThan", "{Field} <  {Value}"},
    {"OnOrBefore%",  "{Field} <  {Value}"},
    {"End%",         "{Field} <  {Value}"},
    {"Stop%",        "{Field} <  {Value}"},
    {"To%",          "{Field} <  {Value}"},
    {"Until%",       "{Field} <  {Value}"},
    {"Max%",         "{Field} <  {Value}"},
    {"Maximum%",     "{Field} <  {Value}"},

    {"%GreaterThanOrEqualTo%", "{Field} >= {Value}"},
    {"%GreaterThan%",          "{Field} >  {Value}"},
    {"%LessThan%",             "{Field} <  {Value}"},
    {"%LessThanOrEqualTo%",    "{Field} <  {Value}"},
    {"%NotEqualTo",            "{Field} <> {Value}"},

    {"Like%",        "UPPER({Field}) LIKE UPPER({Value})"},
    {"%In",          "{Field} IN ({Values})"},
    {"%Ids",         "{Field} IN ({Values})"},
    {"%Between%",    "{Field} BETWEEN {Value1} AND {Value2}"},
    {"%HasAll",      "{Value} & {Field} = {Value}"},
    {"%HasAny",      "{Value} & {Field} > 0"},

    {"%IsNull",      "{Field} IS NULL"},
    {"%IsNotNull",   "{Field} IS NOT NULL"},
};

Each convention key includes % wildcards to define where a DataModel field names can appear, either as a Prefix, Suffix or both. The convention value describes the SQL filter that gets applied to the query when the property is populated.

Properties that matches a DataModel field performs an exact query {Field} = {Value}, e.g:

const api = client.api(new QueryBookings({ id:1 }))

As MinCost matches the "Min%" convention it applies the Cost >= 100 filter to the query:

const api = client.api(new QueryBookings({ minCost:100 }))

As CostBetween matches the "%Between%" convention it applies the Cost BETWEEN 100 AND 200 filter to the query:

const api = client.api(new QueryBookings({ costBetween:[100,200] }))

AutoQuery also matches on pluralized fields where Ids matches Id and applies the Id IN (1,2,3) filter:

const api = client.api(new QueryBookings({ ids:[1,2,3] }))

Multiple Request DTO properties applies mutliple AND filters, e.g:

const api = client.api(new QueryBookings({ minCost:100, ids:[1,2,3] }))

Applies the (Cost >= 100) AND (Id IN (1,2,3)) filter.

Key Conventions

API Client Usage

Frontend code imports from lib/gateway.ts:

import { client } from '@/lib/gateway'
import { QueryBookings } from '@/lib/dtos'

const response = await client.api(new QueryBookings())

The client is a configured JsonServiceClient pointing to /api (proxied to .NET backend).

All .NET APIs are accessible by Request DTOs which implement either a IReturn<ResponseType> a IReturnVoid interface which defines the API Response, e.g:

export class Hello implements IReturn<HelloResponse>, IGet
{
    public name: string;
    public constructor(init?: Partial<Hello>) { (Object as any).assign(this, init); }
}
export class HelloResponse
{
    public result: string;
    public constructor(init?: Partial<HelloResponse>) { (Object as any).assign(this, init); }
}

ServiceStack API Client Pattern

Inside a React Component use useClient() to resolve a Service Client. The ApiResult can hold loading, failed and successful API Response states, e.g:

type Props = { value: string }
export default ({ value }:Props) => {
    const [name, setName] = useState(value)
    const client = useClient()
    const [api, setApi] = useState<ApiResult<HelloResponse>>(new ApiResult())

    useEffect(() => {
        (async () => {
            setApi(new ApiResult())
            setApi(await client.api(new Hello({ name })))
        })()
    }, [name])

    return (<div>
        <TextInput id="name" label="API Example" value={name} onChange={setName} />
        {api.error
            ? <div className="text-red-500">{api.error.message}</div>
            : api.succeeded
                ? <div className="text-gray-900">{api.response.result}</div>
                : <div>loading...</div>}
    </div>)
}

All client api, apiVoid and apiForm methods never throws exceptions - it always returns an ApiResult<T> which contains either a response for successful responses or an error with a populated ResponseStatus, as such using try/catch around client.api* calls is always wrong as it implies it would throw an Exception, when it never does.

The examples below show typical usage:

The api and apiVoid APIs return an ApiResult<Response> which holds both successful and failed API Responses:

const api = await client.api(new Hello({ name }))
if (api.succeeded) {
    console.log(`The API succeded:`, api.response)
} else if (api.error) {
    console.log(`The API failed:`, api.error)
}

The apiForm API can use a HTML Form's FormData for its Request Body together with an APIs empty Request DTO, e.g:

const submit = async (e: React.FormEvent) => {
    const form = e.currentTarget as HTMLFormElement
    const api = await client.apiForm(new CreateContact(), new FormData(form))
    if (api.succeeded) {
        console.log(`The API succeded:`, api.response)
    } else if (api.error) {
        console.log(`The API failed:`, api.error)
    }
}

Using apiForm is required for multipart/form-data File Uploads.

ServiceStack Form Components

The @servicestack/react component library have several components to simplify UI generation.

The <AutoForm> Component can be used to render an API validation bound form for any Request DTO.

import { AutoForm, AutoCreateForm, AutoEditForm, HtmlFormat } from '@servicestack/react'

function GenericFormExample() {
  const [results, setResults] = useState<Booking[]|undefined>()

  const onSuccess = (response:QueryResponse<Booking>) => {
    setResults(response.results)
  }

  return (
   <AutoForm panelClass="mx-auto max-w-3xl" type="QueryBookings" onSuccess={onSuccess} />
   {results && <HtmlFormat value={results} />}
  )
}

The <AutoCreateForm> can be used with an AutoQuery CRUD ICreateDb<T> DTO to render a create entity form.

<AutoCreateForm type="CreateBooking" formStyle="card" />

The <AutoEditForm> can be used with an AutoQuery CRUD IPatchDb<T> or IUpdateDb<T> DTO to render an update form. The deleteType can be set to use an IDeleteDb<T> DTO to enable delete functionality.

function EditFormExample({ booking }:{ Booking:booking }) {
   return (
      <AutoEditForm
         value={booking}
         type="UpdateBooking"
         deleteType="DeleteBooking"
         heading="Change an existing Room Booking"
         subHeading="Manage reservations for MyApp hotels."
         formStyle="card"
      />)
}

ApiStateContext and Input Components

The ApiStateContext can be used to inject an APIs Error ResponseStatus down to all @servicestack/react Input components.

import {ErrorSummary, TextInput, PrimaryButton, useClient, ApiStateContext} from "@servicestack/react"

function CustomFormExample() {
    const client = useClient()
    const [userName, setUserName] = useState<string|undefined>()
    const [password, setPassword] = useState<string|undefined>()

    const onSubmit = async (e:SyntheticEvent<HTMLFormElement>) => {
        e.preventDefault()
        const api = await client.api(new Authenticate({ provider: 'credentials', userName, password }))
        if (api.succeeded) {
            console.log('Signed In!', api.response)
        } else if (api.error.errorCode === 'Unauthorized') {
            console.log('Sign In failed:', api.error.message)
        }
    }

   return (<ApiStateContext.Provider value={client}>
      <form onSubmit={onSubmit}>
        <ErrorSummary except="userName,password,rememberMe"/>
        <div>
            <TextInput id="userName" help="Email you signed up with" autoComplete="email"
                       value={userName} onChange={setUserName}/>
            <TextInput id="password" type="password" help="6 characters or more"
                       value={password} onChange={setPassword}/>
            <PrimaryButton>Log in</PrimaryButton>
        </div>
      </form>
   </ApiStateContext.Provider>)
}

All field errors are displayed next to their Input component all other API errors are displayed with the <ErrorSummary> component. Use except to avoid displaying field errors which are already displayed next to their associated input component.

If needed, the error ResponseStatus can be passed to components using its status property.

Routing

  • /api/* → ServiceStack services
  • /Identity/* → ASP.NET Core Identity Razor Pages
  • /ui/* → ServiceStack API Explorer
  • /admin-ui/* → ServiceStack Admin UI (requires Admin role)
  • /types/typescript → ServiceStack .NET API TypeScript DTOs (for dtos.ts)
  • All other routes → React SPA (via fallback in dev/prod)

Razor Pages Integration

The template includes Razor Pages for Identity UI (/Identity routes) that coexist with the React SPA. These use Tailwind CSS compiled from MyApp/tailwind.input.css to MyApp/wwwroot/css/app.css.

Environment Variables

  • KAMAL_DEPLOY_HOST - Production hostname for deployment

Background Jobs

Configured in Configure.BackgroundJobs.cs using BackgroundsJobFeature. Jobs are commands that implement IAsyncCommand<T>.

Development Workflow

  1. Start dev servers: dotnet watch (starts both .NET and Vite)
  2. Make backend changes: Edit C# files in MyApp.ServiceModel or MyApp.ServiceInterface
  3. Restart .NET Server
  4. Regenerate DTOs: cd MyApp.Client && npm run dtos
  5. Make frontend changes: Edit React files in MyApp.Client/src
  6. Add new CRUD feature:
    • npx okai init Feature
    • Edit MyApp.ServiceModel/Feature.d.ts
    • npx okai Feature.d.ts
    • npm run migrate

Docs: AutoQuery Dev Worfklow

Admin Features

  • /admin-ui - ServiceStack Admin UI (database, users, API explorer)
  • /admin-ui/users - User management (requires Admin role)
  • /up - Health check endpoint

Deployment

GitHub Actions workflows in .github/workflows/ uses Kamal for Deployments:

  • build.yml - CI build and test
  • build-container.yml - Docker image build
  • release.yml - Kamal deployment to production

Configure KAMAL_DEPLOY_HOST in GitHub secrets for your hostname. Kamal config in config/deploy.yml derives service names from repository name.


Using CLAUDE.md with AI Coding Tools

When working with AI assistants, simply reference this file or open it in your editor to provide the AI with full project context:

With Claude Code

# Claude Code automatically reads CLAUDE.md files when available
claude-code

With Cursor

  1. Open CLAUDE.md in your editor
  2. Use Cmd/Ctrl + L to add it to the chat context
  3. Ask questions or request features

With GitHub Copilot Chat

@workspace /explain Check the CLAUDE.md file for project conventions