Le front (Svelte/React/Vue) découplé du back (Symfony) est un standard. Le sujet n’est plus « comment appeler l’API », mais comment le faire proprement, en prod, sans surfaces d’attaque inutiles.
Ce guide donne une convention d’URL simple, deux stratégies d’auth claires (session vs JWT en cookie), le CSRF correctement traité, et des snippets prêts à coller (Symfony, Vite, Nginx, curl).
1 – Convention des endpoints : /api/* ou rien
- Tous les endpoints de données commencent par /api/(ex./api/users,/api/analyse).
- Avantages : sécurité (firewall dédié), cache/ETag séparés, logs distincts, règles CORS ciblées, reverse‑proxy plus simple.
Règle nginx/Apache : toutes les requêtes hors
/api/*servent leindex.htmlde la SPA. Les/api/*vont verspublic/index.php(Symfony).
2 – Cookies HttpOnly : le bon choix en même domaine
Quand front et back partagent le même sous‑domaine, préférez un cookie HttpOnly plutôt que du stockage côté client (localStorage).
Attributs recommandés :
- HttpOnly+- Secure+- Path=/api
- SameSite=Lax(pratique) et CSRF token pour les méthodes mutantes (POST/PUT/PATCH/DELETE).
- TTL court (ex. 15 min) + refresh (rotation) → voir §6.
SameSite=Strictcasse trop de flows légitimes (deeplinks, retours).Lax+ vrai CSRF = combo robuste.
3 – Dev : CORS ciblé + proxy de dev
En dev, origines différentes (ex. Vite :5173 / Symfony :8000).
3.1 CORS côté Symfony (Nelmio)
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET','POST','PUT','PATCH','DELETE','OPTIONS']
allow_headers: ['Content-Type','Authorization',
                'X-CSRF-Token','X-Requested-With']
expose_headers: ['Link']
max_age: 3600
paths:
'^/api/':
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET','POST','PUT','PATCH','DELETE','OPTIONS']
allow_headers: ['Content-Type','Authorization',
                'X-CSRF-Token','X-Requested-With']
allow_credentials: true # indispensable pour envoyer les cookies
# .env.local (dev)
CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1):(3000|5173)$
3.2 Proxy côté Vite/Svelte
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
        secure: false,
      },
    },
  },
}
            
4 – Client Svelte : wrapper fetch (cookies + CSRF + 401
								
// src/lib/api.js
let csrfToken = null;
async function ensureCsrf() {
  if (!csrfToken) {
    const r = await fetch('/api/auth/csrf', { credentials: 'include' });
    if (!r.ok) throw new Error('CSRF fetch failed');
    const { token } = await r.json();
    csrfToken = token;
  }
  return csrfToken;
}
export async function api(path, { method = 'GET', body, headers = {} } = {}) {
  const opts = {
    method,
    credentials: 'include', // envoie le cookie HttpOnly
    headers: { 'Accept': 'application/json', ...headers }
  };
if (body !== undefined) {
  opts.headers['Content-Type'] = 'application/json';
  // token CSRF uniquement sur méthodes mutantes
  if (['POST','PUT','PATCH','DELETE'].includes(method.toUpperCase())) {
    const t = await ensureCsrf();
    opts.headers['X-CSRF-Token'] = t;
  }
  opts.body = JSON.stringify(body);
}
const res = await fetch(path.startsWith('/api') ? path : `/api${path}`, opts);
if (res.status === 401) {
  // TODO: rediriger vers /login, rafraîchir le token, etc.
}
if (!res.ok) {
  const problem = await res.json().catch(() => ({}));
  throw Object.assign(new Error(problem.detail || 'API error'), { problem, status: res.status });
}
if (res.status === 204) return null;
  return res.json();
}
            
5) Security.yaml : choisis l’une des deux options
Option A — Session en cookie (étatful, simple, efficace)
- stateless: false
- CSRF via CsrfTokenManagerou token customX-CSRF-Token.
# config/packages/security.yaml
security:
  firewalls:
    api:
      pattern: ^/api/
      stateless: false
      lazy: true
      user_checker: App\Security\UserChecker
      # Exemple avec authenticator JSON
      custom_authenticators:
        - App\Security\JsonLoginAuthenticator
      logout:
        path: /api/auth/logout
        invalidate_session: true
  access_control:
    - { path: ^/api/auth/(login|csrf)$, roles: PUBLIC_ACCESS }
    - { path: ^/api/, roles: ROLE_USER }
    
Option B — JWT en cookie (stateless)
- stateless: true
- CSRF obligatoire sur méthodes mutantes
- Couple access (court)/refresh (plus long)+ rotation
# config/packages/security.yaml
security:
  firewalls:
    api:
      pattern: ^/api/
      stateless: true
      custom_authenticators:
        - App\Security\JwtCookieAuthenticator
  access_control:
    - { path: ^/api/auth/(login|refresh|csrf)$, 
        roles: PUBLIC_ACCESS }
    - { path: ^/api/, roles: ROLE_USER }
    
Dans les deux cas, impose
Accept: application/jsonet retourne des erreurs RFC 7807 (Problem Details).
Exemple de Problem Details :
{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication required",
  "instance": "/api/users/me"
}
    
6 – Endpoints d’auth clairs (login / refresh / logout / csrf)
- POST /api/auth/login→ set cookie HttpOnly (- Path=/api,- Secure,- SameSite=Lax) + renvoie le profil minimal.
- POST /api/auth/refresh→ rotation des tokens (JWT) ou prolongation de session (rolling) ; protège contre le vol de refresh (stockage côté serveur, liste de révocation courte).
- POST /api/auth/logout→- invalidate_session+ suppression cookie (Max-Age=0).
- GET /api/auth/csrf→ renvoie un token CSRF (JSON) consommable par le header- X-CSRF-Token.
7 – Reverse‑proxy (Nginx) : séparation SPA / API
# /api -> Symfony
location ^~ /api/ {
  proxy_pass http://127.0.0.1:9000;   # PHP-FPM via fastcgi_pass si configuré, ou backend
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# tout le reste -> SPA (SvelteKit static/adapter)
location / {
  try_files $uri $uri/ /index.html;
}
Pensez à bloquer l’accès aux dossiers sensibles (
/var,/config, backups…).
8 – Cache & ETag
- Sur /api/*GET :ETag/Last-Modified+Cache-Control: private, must-revalidate.
- Sur POST/PUT/PATCH/DELETE : Cache-Control: no-store.
- Sur assets SPA (hors /api) : fingerprint +Cache-Control: public, max-age=31536000, immutable.
9 – Tests manuels rapides (sans Postman)
# 1) login et garde les cookies
curl -i -c /tmp/c.jar -X POST \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  --data '{"email":"user@example.com","password":"secret"}' \
  https://example.com/api/auth/login
# 2) récupère le CSRF
curl -b /tmp/c.jar -c /tmp/c.jar -H 'Accept: application/json' \
  https://example.com/api/auth/csrf
# -> { "token": "..." }
# 3) appel protégé avec CSRF (méthode mutante)
curl -b /tmp/c.jar -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -H 'X-CSRF-Token: __TOKEN__' \
  --data '{"title":"Nouvelle ressource"}' \
  https://example.com/api/items
# 4) refresh (si JWT)
curl -b /tmp/c.jar -X POST -H 'Accept: application/json' \
  https://example.com/api/auth/refresh
# 5) logout
curl -b /tmp/c.jar -X POST -H 'Accept: application/json' \
  https://example.com/api/auth/logout
10) Check‑list mise en prod (copy/paste)
- HTTPS + HSTS obligatoires.
- Cookies: HttpOnly + Secure + SameSite=Lax + Path=/api.
- CSRF sur POST/PUT/PATCH/DELETE (/api/auth/csrf→ headerX-CSRF-Token).
- security.yaml: - Session → stateless: false
- JWT cookie → stateless: true+ CSRF obligatoire.
 
- Session → 
- Rate limiter sur /api/auth/login(+Retry-After).
- CORS OFF en prod (même domaine). En dev: origin regex + allow_credentials: true.
- Nginx split : /api/*→ Symfony, le reste → SPA (index.html).
- Cache: GET /api=ETag; mutations =no-store; assets =immutable.
- Erreurs: Problem Details (RFC7807) ; 401≠403.
- Versioning: - /api/v1si public/multi-clients.
Smoke test (3 commandes)
# login (cookies)
curl -i -c c.jar -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' \
  --data '{"email":"u@x.com","password":"secret"}' https://app.tld/api/auth/login
# csrf
curl -b c.jar -H 'Accept: application/json' https://app.tld/api/auth/csrf
# POST protégé
curl -b c.jar -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -H 'X-CSRF-Token: __TOKEN__' --data '{"title":"ok"}' https://app.tld/api/items
Conclusion
Pas de mode : du contexte. Même domaine → cookies HttpOnly.
Session (statefull) si tu veux du simple/robuste, JWT (stateless) si tu as plusieurs clients hétérogènes (mobile, extensions, autres domaines) — mais avec CSRF et refresh propres.
Avec :
- /api/*isolé,
- cookies bien configurés,
- CSRF traité sérieusement,
- proxy propre,
- et snippets prod,
Voilà une base maintenable, sécurisée, et prête pour l’échelle.


 
										