create-dotnetspa

Scaffold a .NET 10 + SPA solution (React, Vue or Angular) - coupled or decoupled, Minimal API or Controllers.
174
Install
npm install create-dotnetspa
Latest Version:1.0.0
License:MIT
Last Updated:Sep 25, 2026
Links: npm
Maintainer: luisfd3v

create-dotnetspa

Scaffold an ASP.NET Core (.NET 10) + SPA solution from the command line, the way create-vite or create-hono do it.

npm create dotnetspa@latest

Why this exists

There is no good official template for this combination, and it is worth being precise about why:

  • dotnet new react and dotnet new angular were discontinued in .NET 8 and no longer ship with the SDK.
  • dotnet new vue never existed.
  • The SPA templates Microsoft still maintains only exist inside Visual Studio and rely on .esproj, so they are unusable from a CLI, from CI, or on macOS and Linux.
  • The third-party alternatives are either stale (the old SPA templates are pinned to Create React App) or paid.

So this CLI generates the whole thing: a solution with src/<Name>.API and src/<Name>.Client, wired together correctly for whichever combination you pick.

Requirements

  • .NET SDK 10 — the CLI fails fast with a clear message otherwise
  • Node.js 20+

Usage

Interactive:

npm create dotnetspa@latest

Non-interactive, for CI or scripting:

npm create dotnetspa@latest -- \
  --name MyProject \
  --framework vue \
  --coupling coupled \
  --api minimal \
  -y

Options

Option Values Default Notes
[directory] path ./<project-name> Where to generate the project
--name <name> valid C# identifier (prompted) Drives every other name
-f, --framework vue, react, angular (prompted)
-c, --coupling coupled, decoupled (prompted)
-a, --api minimal, controllers (prompted)
-p, --pm npm, pnpm, yarn, bun detected from the invoking PM
--sln slnx, sln slnx .slnx is the .NET 10 default
--no-scalar scalar enabled Skips the Scalar API reference UI
--no-install install enabled Skips dotnet restore and the SPA install
-y, --yes Never prompt; requires --name
--force Allow a non-empty target directory

--yes is genuinely non-interactive: it never prompts, and it errors instead of hanging when stdin is not a TTY.

Naming convention

You provide one name; everything else derives from it following the usual .NET convention.

Input Result
--name MyProject MyProject.slnx
src/MyProject.API
src/MyProject.Client
npm package my-project-client

What gets generated

MyProject/
├── MyProject.slnx
├── .editorconfig
├── .gitignore
├── README.md
└── src/
    ├── MyProject.API/          # ASP.NET Core, plus the coupling wiring
    └── MyProject.Client/       # Vue / React / Angular

Backend — generated by dotnet new webapi, so the target framework, launch settings, implicit usings and OpenAPI wiring always match the installed SDK. The CLI then replaces the throw-away weather sample with a small products slice that follows the structure you chose:

  • minimal: endpoint groups in Endpoints/ProductEndpoints.cs
  • controllers: Controllers/ProductsController.cs with Models/ and Services/

Frontend — our own Vite (or Angular CLI) templates, pre-wired: dev-server proxy for /api, a typed API client, two routed pages, and one page that actually calls the backend.

Coupled vs decoupled

Both modes use the same layout. Only the wiring differs, so you can move between them later without reorganising anything.

Coupled

The API hosts the SPA. dotnet run starts the Vite dev server for you:

dotnet run --project src/MyProject.API --launch-profile https

This works through Microsoft.AspNetCore.SpaProxy. One detail is worth knowing, because it explains the whole design:

SpaProxyMiddleware compares the request path against new Uri(ServerUrl).LocalPath. With SpaProxyServerUrl = http://localhost:5173 that is just /. It only ever intercepts the root path — it does not proxy /api.

So the flow is: the browser is redirected to the Vite dev server, and from then on the SPA runs there. Its /api/* calls go to Vite, and the Vite dev server proxies them back to Kestrel. That is why the generated client always uses a relative /api base URL, and why coupled and decoupled behave identically in development.

In production the API serves the SPA from wwwroot:

dotnet publish src/MyProject.API -c Release

A publish target runs the SPA install and build and packs the bundle into wwwroot, giving you one deployable unit.

Decoupled

Two independent apps. Run them side by side:

# terminal 1
dotnet run --project src/MyProject.API --launch-profile https

# terminal 2
cd src/MyProject.Client && npm run dev

No SpaProxy, no static files in the API, no publish target. A ClientApp CORS policy allows the dev origin in case you call the API directly; the dev-server proxy means you usually do not need it.

How it is put together

  • create-hono's shape: a thin CLI over a set of templates.
  • @clack/prompts for the interactive flow (the intro/spinner/tasks/note look).
  • dotnet new scaffolds the .NET shell; the CLI post-processes only the parts that no template provides.
  • Templates live in templates/. templates/*/_shared holds files common to a variant; the variant folder is copied afterwards and wins, so it can override anything.
  • Token replacement uses __TOKEN__, never {{ }} — Vue and Angular use {{ }} for interpolation and would be corrupted.

Development

npm install
npm run build      # bundle the CLI into dist/
npm run dev        # rebuild on change
npm test           # vitest
npm run typecheck  # tsc --noEmit

Templates are read from templates/ at runtime, so editing a template does not require a rebuild — only changes under src/ do.

To regenerate a project into a scratch directory:

node dist/cli.js --name Teste -f vue -c coupled -a minimal -y --force

Verified combinations

Each cell was generated and then built for real (dotnet build, the SPA production build, and dotnet publish for the coupled rows):

Framework Coupling API
Vue coupled minimal
React decoupled controllers
Angular coupled minimal

Releasing

Releases are automatic. On every push to main, .github/workflows/release.yml runs semantic-release, which reads the commit messages to work out the next version, updates CHANGELOG.md, tags the release, opens a GitHub Release and publishes to npm.

The commit message is the only input to that decision:

Commit Release
fix: ... patch — 1.0.0 → 1.0.1
perf: ... patch
feat: ... minor — 1.0.0 → 1.1.0
feat!: ... or a BREAKING CHANGE: footer major — 1.0.0 → 2.0.0
docs:, chore:, ci:, test:, refactor:, style:, build: nothing published

Not every merge publishes a version, and that is intended — otherwise the number would stop meaning anything.

Because the project uses squash merges, the pull request title becomes the commit message on main, so that is what actually decides the release. CI lints both every commit in the PR and the PR title itself; local commits are checked by a commit-msg git hook installed by npm install.

Publishing authenticates with npm through OIDC trusted publishing, so there is no NPM_TOKEN secret in this repository and every published version carries a provenance attestation. See CONTRIBUTING.md for the contributor workflow and the required npm settings.

Known limitations

  • Template dependency versions are pinned by hand, so they drift. Run a generated project's build before releasing a new framework major.
  • Angular is pinned to the 22.x line, which is validated; newer majors may change the angular.json schema.
  • The coupled publish runs the package manager's install on every publish. Point it at npm ci if you want lockfile-exact installs in CI.

Ideas that are not built yet

  • Publish the same templates as a NuGet template package, so dotnet new dotnetspa works for people who do not use Node.
  • Fetch templates from a git ref (like create-hono does with giget) so template fixes ship without an npm release.
  • Optional --force-style migration between coupled and decoupled after the fact.

Related Packages

create-vue_zx

Vue 3 + Vite 标准化脚手架 — 快速搭建 Vue 3 项目开发环境

1,457
@moluoxixi/create-app

Create supported Vite projects with an atomic layered architecture

4,573 0
@singapore-tech/create-app

Create a SIT web app (Vite + React or Vue + Tailwind v4 + SIT Design System) wit...

0
create-component-template

Generates react components from templates

1,172 3
@richscripts/create-richtexteditor

Scaffold a working RichTextEditor example app for React, Vue, Angular, or Svelte...

448 2

Version History

Version Released License
1.0.0 MIT