This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a ServiceStack Angular SPA template combining .NET 10.0 backend with Angular 21 frontend. The project uses a layered architecture with clear separation between API, business logic, DTOs, and frontend.
# 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 dtosTailwind CSS (for Razor Pages):
# Watch mode for styling (from MyApp directory)
npm run ui:dev
# Production build
npm run ui:buildFull development setup: Run .NET backend, Razor Pages and Node Vite dev server in parallel:
cd MyApp && dotnet watchUsing tailwind in new Razor Pages:
Terminal 2: cd MyApp && npm run ui:dev (Tailwind watch)
Backend:
# Run all .NET tests
cd MyApp.Tests && dotnet testFrontend:
# Run Karma/Jasmine tests (from MyApp.Client directory)
npm test# 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# 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.tsDevelopment Mode:
dotnet watchfrom MyApp starts .NET (port 5001) and Angular dev server (port 4200), accessible viahttps://127.0.0.1:4200- ASP.NET Core proxies requests to Angular dev server via
NodeProxy(configured in Program.cs) - Hot Module Replacement (HMR) enabled via WebSocket proxying using
MapNotFoundToNode,MapViteHmr,RunNodeProcess,MapFallbackToNodein Program.cs
Production Mode:
- Angular builds app to
MyApp.Client/dist/, which is copied toMyApp/wwwroot/when published - ASP.NET Core serves static files directly from
wwwroot- no Node.js required - Fallback to
index.htmlfor client-side routing
AppHost uses .NET's IHostingStartup pattern to split configuration across multiple files in MyApp/:
- Configure.AppHost.cs - Main ServiceStack AppHost registration
- Configure.Auth.cs - ServiceStack AuthFeature with ASP.NET Core Identity integration
- Configure.AutoQuery.cs - AutoQuery features and audit events
- Configure.Db.cs - Database setup (OrmLite for app data, EF Core for Identity)
- Configure.Db.Migrations.cs - Runs OrmLite and EF DB Migrations and creates initial users
- Configure.BackgroundJobs.cs - Background job processing
- Configure.HealthChecks.cs - Health monitoring endpoint
This pattern keeps Program.cs clean and separates concerns. Each Configure.*.cs file is auto-registered via [assembly: HostingStartup] attribute.
MyApp/ # .NET Backend (hosts both .NET and Angular build output)
├── 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/ # Angular Frontend
├── src/
│ ├── app/ # Angular components and pages
│ ├── services/ # Shared services including auth (signal-based)
│ ├── components/ # Angular components
├── dtos.ts # Auto-generated from C# (via `npm run dtos`)
├── styles/ # Tailwind CSS
└── angular.json # Angular CLI configuration
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
Angular Standalone Components:
- No NgModules, all components are standalone
- Signal-based state management (Angular 21+)
- Router configuration in
app.routes.ts
Key Services:
auth.service.ts- Authentication with signal-based state- ServiceStack client for API communication
Routing:
- Client-side routing handled by Angular Router
- Fallback routing in production via
MapFallbackToFile("index.html")
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 IdentityMyApp/Migrations/Migration1000.cs- OrmLite migration for app tables (e.g., Booking)
Run npm run migrate to execute both.
- ASP.NET Core Identity handles user registration/login via Razor Pages at
/Identity/*routes - ServiceStack AuthFeature integrates with Identity via
IdentityAuth.For<ApplicationUser>()in Configure.Auth.cs - Custom claims added via
AdditionalUserClaimsPrincipalFactoryandCustomUserSession - ServiceStack services use
[ValidateIsAuthenticated]and[ValidateHasRole]attributes for authorization (see Bookings.cs)
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.
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.
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>
{
//...
}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.
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.
ServiceStack's AutoQuery generates full CRUD APIs from declarative request DTOs. Example in Bookings.cs:
QueryBookings : QueryDb<Booking>→ GET /api/QueryBookings with filtering/sorting/pagingCreateBooking : ICreateDb<Booking>→ POST /api/CreateBookingUpdateBooking : IPatchDb<Booking>→ PATCH /api/UpdateBookingDeleteBooking : IDeleteDb<Booking>→ DELETE /api/DeleteBooking
No service implementation required - AutoQuery handles it. Audit fields (CreatedBy, ModifiedBy, etc.) auto-populated via [AutoApply(Behavior.AuditCreate)] attributes.
After changing C# DTOs in MyApp.ServiceModel/, restart the .NET Server then run:
cd MyApp.Client && npm run dtosThis calls ServiceStack's /types/typescript endpoint and updates dtos.ts with type-safe client DTOs. The Vite dev server auto-reloads.
The npx okai tool generates C# AutoQuery APIs and migrations from TypeScript data models (.d.ts files):
- TypeScript data model (
MyApp.ServiceModel/Bookings.d.ts) defines the entity with decorators - C# AutoQuery APIs (
MyApp.ServiceModel/Bookings.cs) - auto-generated CRUD request/response DTOs - 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.
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 multiple 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.
Frontend code imports from lib/gateway.ts:
import { QueryBookings } from '@/lib/dtos'
private client = inject(JsonServiceClient);
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); }
}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 succeeded:`, 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: Event) => {
const form = e.currentTarget as HTMLFormElement
const api = await client.apiForm(new CreateContact(), new FormData(form))
if (api.succeeded) {
console.log(`The API succeeded:`, api.response)
} else if (api.error) {
console.log(`The API failed:`, api.error)
}
}Using apiForm is required for multipart/form-data File Uploads.
/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 → Angular SPA (via fallback in dev/prod)
The template includes Razor Pages for Identity UI (/Identity routes) that coexist with the Angular SPA. These use Tailwind CSS compiled from MyApp/tailwind.input.css to MyApp/wwwroot/css/app.css.
KAMAL_DEPLOY_HOST- Production hostname for deployment
Configured in Configure.BackgroundJobs.cs using BackgroundsJobFeature. Jobs are commands that implement IAsyncCommand<T>.
- Start dev servers:
dotnet watch(starts both .NET and Vite) - Make backend changes: Edit C# files in
MyApp.ServiceModelorMyApp.ServiceInterface - Restart .NET Server
- Regenerate DTOs:
cd MyApp.Client && npm run dtos - Make frontend changes: Edit Angular files in
MyApp.Client/src - Add new CRUD feature:
npx okai init Feature- Edit
MyApp.ServiceModel/Feature.d.ts npx okai Feature.d.tsnpm run migrate
Docs: AutoQuery Dev Workflow
/admin-ui- ServiceStack Admin UI (database, users, API explorer)/admin-ui/users- User management (requires Admin role)/up- Health check endpoint
GitHub Actions workflows in .github/workflows/ uses Kamal for Deployments:
build.yml- CI build and testbuild-container.yml- Docker image buildrelease.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.
Template demo available at: https://angular-spa.web-templates.io