Démarrer un projet client .NET / React correctement, c’est éviter des semaines de dette technique plus tard. Mon objectif au kick-off : avoir en quelques jours un socle clair, sécurisé, observable et déjà branché à une CI/CD.
Voici comment je démarre un projet client en 5 étapes, avec scripts, exemples de code et petits choix d’architecture.
1) Squelette & outils
Avant d’écrire la moindre feature, je pose un squelette simple, compréhensible par toute l’équipe :
MyApp.sln
/api // ASP.NET Core 8 Minimal API
/web // React + TypeScript (Vite)
/shared // (optionnel) contrats/DTO partagés via package
Initialiser l’API (.NET 8)
Je pars généralement sur une Minimal API avec Swagger, validation, et déjà prête pour l’observabilité.
dotnet new web -n api
cd api
dotnet add package Swashbuckle.AspNetCore
dotnet add package FluentValidation.DependencyInjectionExtensions
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package Serilog.AspNetCore
Dans Program.cs, je pose tout de suite un endpoint de santé et une route de test :
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.MapGet("/api/info", () => new
{
Name = "MyApp",
Version = "0.0.1",
Utc = DateTime.UtcNow
});
app.Run();
Initialiser le front (React + Vite + TS)
Côté front, j’utilise Vite pour avoir un setup léger et rapide, avec React Router et React Query.
npm create vite@latest web -- --template react-ts
cd web
npm i @tanstack/react-query react-router-dom zod
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
Je crée ensuite une structure minimale :
web/src
/components
/pages
/routes
/api
main.tsx
Le tout premier écran se contente souvent d’appeler l’endpoint /api/info pour
vérifier que le pipe front <-> back fonctionne bien.
// web/src/api/http.ts
export async function get<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, {
...init,
headers: {
"Accept": "application/json",
...(init?.headers || {})
}
});
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json() as Promise<T>;
}
// web/src/pages/Home.tsx
import { useEffect, useState } from "react";
import { get } from "../api/http";
type Info = {
name: string;
version: string;
utc: string;
};
export function Home() {
const [info, setInfo] = useState<Info | null>(null);
useEffect(() => {
get<Info>("/api/info").then(setInfo).catch(console.error);
}, []);
if (!info) return <p>Chargement…</p>;
return (
<div>
<h1>{info.name}</h1>
<p>Version: {info.version}</p>
<p>Serveur UTC: {new Date(info.utc).toLocaleString()}</p>
</div>
);
}
2) Sécurité de base (CORS, rate-limit, headers)
Même en tout début de projet, je pose un minimum de sécurité et d’hygiène HTTP : CORS strict, limite de requêtes, headers de protection.
CORS & rate limiting
Je branche une policy CORS pour le front et un rate limit global simple :
// Program.cs
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
builder.Services.AddCors(o => o.AddPolicy("spa", p =>
p.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()));
builder.Services.AddRateLimiter(o =>
{
o.GlobalLimiter = PartitionedRateLimiter.CreateHttpContextLimiter(_ =>
RateLimitPartition.GetFixedWindowLimiter("global", _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 120,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
}));
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseCors("spa");
app.UseRateLimiter();
Headers de protection
J’ajoute aussi quelques headers de sécurité de base (à ajuster selon les besoins) :
app.Use(async (ctx, next) =>
{
ctx.Response.Headers["X-Frame-Options"] = "DENY";
ctx.Response.Headers["X-Content-Type-Options"] = "nosniff";
ctx.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
await next();
});
L’authentification (JWT / OAuth2) arrive en général juste après ce socle, dès qu’une première feature sensible arrive. Mais même avant ça, ces protections évitent déjà beaucoup de mauvaises surprises.
3) Observabilité : logs, traces, métriques
Je veux pouvoir répondre à la question : « que s’est-il passé pour ce client à 10h32 ? ». Pour ça, je branche immédiatement OpenTelemetry et des logs corrélés.
OpenTelemetry minimal
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using OpenTelemetry.Resources;
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("MyApp.Api"))
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation())
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation());
En environnement cloud, je configure un export OTLP vers Application Insights,
Grafana, Datadog… En local, je reste parfois sur un simple logging console +
dotnet-counters pour les premiers tests.
Logs corrélés par requête
app.MapGet("/api/orders/{id:int}", (int id, ILogger<Program> logger) =>
{
logger.LogInformation("Fetching order {OrderId}", id);
// ... appel BDD / service ...
var order = new { Id = id, Total = 120.50m };
logger.LogInformation("Order {OrderId} found with total {Total}", id, order.Total);
return Results.Ok(order);
});
Avec ça, dès le premier bug remonté par le métier, j’ai déjà de quoi investiguer sans « deviner » ce qui se passe.
4) Front prêt prod : routing, fetch, cache & UX
Côté front, l’objectif est d’avoir rapidement une base propre : routing clair, gestion de l’état côté serveur (React Query), et un minimum de UX (loading, erreurs).
Routing de base
// web/src/routes/AppRoutes.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home } from "../pages/Home";
import { NotFound } from "../pages/NotFound";
export function AppRoutes() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
React Query pour les appels API
// web/src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AppRoutes } from "./routes/AppRoutes";
const client = new QueryClient();
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={client}>
<AppRoutes />
</QueryClientProvider>
</React.StrictMode>
);
// web/src/api/hooks.ts
import { useQuery } from "@tanstack/react-query";
import { get } from "./http";
type Info = {
name: string;
version: string;
utc: string;
};
export function useAppInfo() {
return useQuery({
queryKey: ["info"],
queryFn: () => get<Info>("/api/info"),
staleTime: 60_000
});
}
// web/src/pages/Home.tsx (version React Query)
import { useAppInfo } from "../api/hooks";
export function Home() {
const { data, isLoading, error } = useAppInfo();
if (isLoading) return <p>Chargement…</p>;
if (error) return <p>Une erreur est survenue.</p>;
return (
<div>
<h1>{data?.name}</h1>
<p>Version: {data?.version}</p>
</div>
);
}
Avec ça, le front est déjà structuré comme en prod, et il sera très facile d’ajouter de nouvelles pages / features.
5) CI/CD minimale (GitHub Actions)
Dès le début du projet, je branche une CI simple : build + tests API, build front. C’est ce qui évite les « ça marche sur ma machine ».
# .github/workflows/ci.yml
name: ci
on:
push:
branches: [ main, develop ]
pull_request:
jobs:
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Restore
run: dotnet restore api
- name: Build
run: dotnet build api -c Release --no-restore
- name: Tests
run: dotnet test api -c Release --no-build
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install & Build
run: |
cd web
npm ci
npm run build
Plus tard, je rajoute les jobs de déploiement (Azure Web App, containers, static hosting pour le front…), mais cette première CI suffit à garder le projet sain dès le début.
Bilan : ce qu’on a après quelques jours
- Un squelette .NET / React propre, versionné et reproductible.
- Une API déjà protégée (CORS, rate-limit, headers) et observable.
- Un front React organisé (routing, React Query, helpers API).
- Une CI GitHub Actions qui valide le build à chaque push.
À partir de là, je peux attaquer les vraies features métier sereinement : le socle technique est en place, et chaque nouvelle brique viendra s’y brancher proprement, sans bricolage.