Toate ghidurile

Cum proiectezi un API REST bun

Resurse, metode HTTP, coduri de status, erori, paginare, versionare și OpenAPI, cu reguli și exemple practice.


Un API este un contract: odată ce alți developeri, sau aplicația ta mobilă instalată pe mii de telefoane, depind de el, fiecare decizie proastă devine greu de reparat. Vestea bună este că un API REST bun nu presupune nimic exotic, ci aplicarea consecventă a câtorva reguli despre resurse, metode HTTP, coduri de status și erori. Ghidul de față le adună pe toate, cu exemple pe care le poți folosi direct.

Ce înseamnă REST, pe scurt

REST (Representational State Transfer) este un stil arhitectural descris de Roy Fielding în teza sa de doctorat din 2000. Ideile care contează în practică:

  • Resurse. Totul este o resursă identificată printr-un URL: un utilizator, o comandă, o listă de produse.
  • Reprezentări. Clientul nu primește resursa în sine, ci o reprezentare a ei, de obicei JSON.
  • Interfață uniformă. Operațiile se exprimă prin metodele standard HTTP, nu prin verbe inventate în URL.
  • Stateless. Fiecare cerere conține tot ce îi trebuie serverului ca s-o proceseze, inclusiv autentificarea. Serverul nu ține minte „conversația”.

Puține API-uri respectă REST ad litteram (de exemplu principiul HATEOAS, cu link-uri navigabile în fiecare răspuns). Când oamenii spun „API REST”, se referă de obicei la un API HTTP orientat pe resurse, care vorbește JSON. Despre acesta este vorba aici.

Resursele: substantive, nu verbe

URL-ul descrie ce, metoda HTTP descrie ce faci cu el.

# Așa nu
POST /getUsers
POST /createOrder
GET  /deleteProduct?id=42

# Așa da
GET    /users
POST   /orders
DELETE /products/42

Reguli care te scutesc de discuții lungi în code review:

  • Plural pentru colecții: /users, /users/42. Consecvența contează mai mult decât alegerea în sine.
  • Litere mici și kebab-case în path: /learning-paths, nu /LearningPaths.
  • Imbricare cu măsură. /users/42/orders este clar; /users/42/orders/7/items/3/reviews este prea mult. După un nivel, preferă resurse de sine stătătoare (/order-items/3) sau filtre (/reviews?orderItemId=3).
  • Fără extensii de tipul /users.json; formatul se negociază prin header-ul Accept.
  • Acțiunile care nu se potrivesc în CRUD pot deveni sub-resurse, POST /orders/7/cancellation, sau, pragmatic, POST /orders/7/cancel. Ambele variante se întâlnesc; important este să alegi una și s-o aplici peste tot.

Pentru numele câmpurilor din JSON alege o convenție (camelCase sau snake_case) și respect-o în toate endpoint-urile.

Metodele HTTP și semantica lor

MetodăFolosireSafeIdempotentăRăspuns tipic
GETCitește o resursă sau o colecțieDaDa200
POSTCreează o resursă sau declanșează o acțiuneNuNu201 + Location
PUTÎnlocuiește complet resursaNuDa200 sau 204
PATCHModifică parțial resursaNuNu neapărat200
DELETEȘterge resursaNuDa204
  • Safe înseamnă că cererea nu modifică nimic pe server. Un GET care șterge date este o capcană: crawler-ele, prefetch-ul și cache-urile presupun că GET este inofensiv.
  • Idempotentă înseamnă că aceeași cerere trimisă de mai multe ori are același efect ca trimisă o singură dată. DELETE /products/42 repetat lasă produsul tot șters. Proprietatea aceasta permite reîncercarea sigură după un timeout.

Crearea unei resurse, făcută corect:

POST /orders HTTP/1.1
Content-Type: application/json

{ "productId": "prd_81", "quantity": 2 }
HTTP/1.1 201 Created
Location: /orders/ord_1029
Content-Type: application/json

{ "id": "ord_1029", "productId": "prd_81", "quantity": 2, "status": "pending" }

Răspunsul returnează resursa creată, inclusiv câmpurile generate de server (id, status), ca clientul să nu fie nevoit să facă imediat încă un GET.

Coduri de status care spun adevărul

Un API care răspunde 200 OK cu { "error": "not found" } în body obligă fiecare client să parseze body-ul ca să afle dacă cererea a reușit. Folosește codurile pentru ce au fost gândite:

CodCând îl folosești
200 OKCerere reușită, cu body
201 CreatedResursă creată; include header-ul Location
204 No ContentReușită, fără body (de exemplu după DELETE)
400 Bad RequestCerere malformată: JSON invalid, parametri lipsă
401 UnauthorizedAutentificarea lipsește sau este invalidă
403 ForbiddenUtilizatorul este autentificat, dar nu are voie
404 Not FoundResursa nu există
409 ConflictConflict cu starea curentă (de exemplu e-mail deja folosit)
422 Unprocessable ContentJSON valid, dar datele nu trec validarea
429 Too Many RequestsLimita de cereri a fost depășită; adaugă Retry-After
500 Internal Server ErrorEroare neașteptată pe server
503 Service UnavailableServiciul este temporar indisponibil

Diferența dintre 401 și 403 apare la aproape orice interviu: 401 înseamnă „nu știu cine ești”, 403 înseamnă „știu cine ești, dar nu ai voie”. Pentru resurse private se răspunde uneori intenționat cu 404 în loc de 403, ca să nu confirmi că resursa există.

Erori consistente: Problem Details

Clienții tăi vor scrie cod care tratează erorile, așa că formatul lor trebuie să fie previzibil. Există un standard pentru asta, RFC 9457: Problem Details for HTTP APIs, cu tipul de conținut application/problem+json:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/validation",
  "title": "Datele trimise nu sunt valide",
  "status": 422,
  "detail": "Câmpul quantity trebuie să fie între 1 și 10.",
  "instance": "/orders",
  "errors": [
    { "field": "quantity", "message": "Valoarea maximă este 10" }
  ]
}

Câmpurile type, title, status, detail și instance sunt definite de standard, iar extensiile precum errors sunt permise. Încă două reguli:

  • Nu expune stack trace-uri, interogări SQL sau mesaje interne în răspunsuri. Le loghezi pe server, cu un identificator de corelare pe care îl poți returna și clientului.
  • Mesajele de validare trebuie să spună ce câmp este greșit și de ce, astfel încât un frontend să le poată afișa lângă câmpul respectiv.

Paginare, filtrare și sortare

Nicio colecție nu rămâne mică. Paginează de la prima versiune, chiar dacă azi ai doar 20 de înregistrări.

Offset pagination este simplă și permite salt direct la pagina N:

GET /products?limit=20&offset=40 HTTP/1.1

Problema: la offset-uri mari baza de date parcurge și aruncă toate rândurile anterioare, iar dacă între timp se inserează elemente, utilizatorul vede duplicate sau sare peste unele.

Cursor pagination (keyset) folosește un marcaj opac al ultimului element văzut:

GET /products?limit=20&cursor=eyJpZCI6InByZF8xMDIwIn0 HTTP/1.1
{
  "data": [
    { "id": "prd_1021", "name": "Tastatură mecanică" }
  ],
  "nextCursor": "eyJpZCI6InByZF8xMDQwIn0"
}

Este stabilă și rapidă pe volume mari, dar nu permite salt direct la o pagină anume. Pentru feed-uri și liste cu scroll infinit este alegerea potrivită.

Pentru filtrare și sortare, query string-ul este locul natural:

GET /products?category=laptops&minPrice=2000&sort=-createdAt,name HTTP/1.1

Convenția cu - pentru sortare descrescătoare este răspândită. Validează câmpurile acceptate la sortare și filtrare după o listă fixă: nu trimite orbește în SQL numele unei coloane venit din URL.

Versionare și compatibilitate

Odată publicat, un API nu se mai poate schimba după bunul plac. Distinge între:

  • Schimbări compatibile: un câmp nou în răspuns, un endpoint nou, un parametru opțional nou. Clienții existenți continuă să funcționeze, cu condiția să ignore câmpurile pe care nu le cunosc.
  • Breaking changes: redenumirea sau ștergerea unui câmp, schimbarea tipului, un parametru devenit obligatoriu, alt cod de status pentru aceeași situație.

Pentru breaking changes ai nevoie de o versiune nouă. Variantele uzuale:

StrategieExempluObservații
În path/v1/ordersCea mai vizibilă, ușor de testat din browser și din curl
În header-ul Acceptapplication/vnd.example.v2+jsonURL-uri curate, dar mai greu de depanat
Pe dată, într-un headerApi-Version: 2026-01-15Granulară, dar cere disciplină internă

Indiferent de variantă, anunță depreciarea din timp, documentează migrarea și păstrează versiunea veche activă o perioadă rezonabilă.

Idempotență și reîncercări

Rețelele pică. Dacă un client trimite POST /payments și primește timeout, nu știe dacă plata s-a făcut, iar o reîncercare naivă poate dubla plata. Soluția răspândită este header-ul Idempotency-Key, popularizat de API-urile de plăți precum Stripe și aflat în curs de standardizare la IETF:

POST /payments HTTP/1.1
Idempotency-Key: 5f8c2a3e-7b1d-4c9a-9e2f-1a2b3c4d5e6f
Content-Type: application/json

{ "amount": 4900, "currency": "RON" }

Serverul salvează cheia împreună cu rezultatul. Dacă aceeași cheie vine din nou, returnează rezultatul salvat în loc să proceseze plata a doua oară. Observă și suma: 4900 înseamnă 49,00 lei, exprimată în bani, ca număr întreg. Sumele de bani nu se transmit ca numere în virgulă mobilă.

Securitate și limite

  • Doar HTTPS, fără excepții, inclusiv pentru mediile de test expuse public.
  • Autentificarea în header, de exemplu Authorization: Bearer <token>, niciodată în query string: URL-urile ajung în loguri, în istoricul browserului și în header-ul Referer.
  • Autorizare pe fiecare resursă. Faptul că utilizatorul este logat nu înseamnă că are voie la /orders/1029. Verifică explicit că resursa îi aparține; altfel ai o vulnerabilitate de tip IDOR (Insecure Direct Object Reference).
  • Rate limiting. Răspunde cu 429 și Retry-After, ca să protejezi serviciul de abuz și de clienții scriși greșit.
  • Validarea intrării după o schemă (JSON Schema, Zod, Pydantic) și respingerea câmpurilor neașteptate, ca să eviți mass assignment: un "role": "admin" strecurat într-un PATCH /users/me nu trebuie să ajungă în baza de date.
  • Limite de dimensiune pentru body-uri și pagini, de exemplu limit maxim 100.

Documentația: OpenAPI

Un API fără documentație este un API pe care nimeni nu îl folosește corect. Standardul de facto este OpenAPI, o descriere în YAML sau JSON a endpoint-urilor, parametrilor și schemelor de date:

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
paths:
  /orders/{orderId}:
    get:
      summary: Returnează o comandă
      parameters:
        - name: orderId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Comanda a fost găsită
        "404":
          description: Comanda nu există

Din aceeași specificație poți genera documentație interactivă, clienți pentru diverse limbaje și teste de contract. Multe framework-uri, precum FastAPI, NestJS, ASP.NET Core sau Spring Boot (cu springdoc-openapi), o pot genera direct din cod. Specificația și uneltele asociate sunt descrise pe openapis.org.

Checklist înainte de lansare

ZonăVerificare
URL-uriSubstantive la plural, kebab-case, imbricare de cel mult un nivel
MetodeGET fără efecte secundare, PUT și DELETE idempotente
StatusCoduri corecte, 201 + Location la creare
EroriFormat unic (Problem Details), fără detalii interne
ColecțiiPaginare cu limită maximă, filtre și sortare validate
EvoluțieStrategie de versionare stabilită înainte de primul client
SecuritateHTTPS, autorizare per resursă, rate limiting, validare după schemă
DocumentațieSpecificație OpenAPI actualizată automat

Când ai nelămuriri despre semantica exactă a unei metode sau a unui cod de status, sursa de referință este RFC 9110: HTTP Semantics.

Pașii următori

  • Construiește un API mic, de exemplu o listă de sarcini cu utilizatori, aplicând checklist-ul de mai sus, apoi scrie-i specificația OpenAPI și testează-l cu curl.
  • Pentru fundamentele de server (baze de date, autentificare, testare), urmează roadmap-ul Backend; dacă lucrezi în JavaScript, continuă cu roadmap-ul Node.js.
  • Când API-ul începe să crească (cache, rate limiting distribuit, cozi de mesaje), treci la System Design.