Spune exact ce vrei
Un API REST vorbește prin două vocabulare. Metoda spune ce vrea clientul: GET citește, POST creează, PUT înlocuiește, PATCH schimbă o parte, DELETE șterge. Codul de stare spune cum a decurs: 2xx succes, 4xx greșeala clientului, 5xx a serverului.
Un detaliu care merită știut: PUT este idempotent — trimis de două ori se termină la fel ca o singură dată. POST nu este: două cereri POST creează două resurse.
Metoda potrivită
Routerul potrivește atât metoda, cât și calea, așa că fiecare verb primește propriul handler. Crearea răspunde cu 201 Created și indică noua resursă printr-un antet Location; ștergerea răspunde cu 204 No Content — succes fără nimic de spus, așa că trupul răspunsului rămâne gol.
GET, POST și DELETE pe aceeași resursă
Trei verbe, trei răspunsuri: lista vine înapoi cu 200, crearea cu 201 și un antet Location, ștergerea cu 204 și un trup gol.
Ieșire: program TypeScript și rezultatul capturat
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
type RouteHandler = (
req: IncomingMessage,
res: ServerResponse,
params: Record<string, string>,
) => void
interface Route {
method: string
pattern: string
handler: RouteHandler
}
function matchPath(pattern: string, path: string): Record<string, string> | undefined {
const want = pattern.split("/")
const got = path.split("/")
if (want.length !== got.length) return undefined
const params: Record<string, string> = {}
for (let i = 0; i < want.length; i++) {
if (want[i].startsWith("{") && want[i].endsWith("}")) {
params[want[i].slice(1, -1)] = got[i]
} else if (want[i] !== got[i]) {
return undefined
}
}
return params
}
function router(routes: Route[]) {
return (req: IncomingMessage, res: ServerResponse): void => {
const url = new URL(req.url ?? "/", "http://localhost")
for (const route of routes) {
const params = matchPath(route.pattern, url.pathname)
if (params !== undefined && route.method === req.method) {
route.handler(req, res, params)
return
}
}
res.writeHead(404).end("negăsit\n")
}
}
const routes: Route[] = [
{
method: "GET",
pattern: "/notes",
handler: (req, res) => {
res.end("notițe: cumpărături, idei\n")
},
},
{
method: "POST",
pattern: "/notes",
handler: (req, res) => {
// 201 Created, iar antetul Location indică noua resursă.
res.writeHead(201, { Location: "/notes/3" }).end("creat\n")
},
},
{
method: "DELETE",
pattern: "/notes/{id}",
handler: (req, res) => {
// 204 No Content: succes fără nimic de spus.
res.writeHead(204).end()
},
},
]
async function main(): Promise<void> {
const server = createServer(router(routes))
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const base = `http://localhost:${(server.address() as { port: number }).port}`
const list = await fetch(`${base}/notes`)
console.log("GET /notes ->", list.status, (await list.text()).trim())
const created = await fetch(`${base}/notes`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: '{"text": "sună-l pe Ben"}',
})
console.log("POST /notes ->", created.status, "Location:", created.headers.get("location"))
const removed = await fetch(`${base}/notes/3`, { method: "DELETE" })
console.log("DELETE /notes/3 ->", removed.status, "(fără conținut)")
server.close()
}
main()
Codul de stare potrivit
Și erorile merită precizie: o resursă lipsă înseamnă 404 Not Found, cu un trup de eroare în JSON, ca să poată fi citit de programe. Un verb greșit pe o cale cunoscută înseamnă 405 Method Not Allowed.
Aici nimeni nu trimite acel 405 în locul tău. Routerul verifică separat cele două părți: dacă s-a potrivit calea vreunei rute, dar nicio rută nu a potrivit metoda, el răspunde cu 405 și adaugă un antet Allow care enumeră verbele care ar fi funcționat.
200, 404 și 405
Notița existentă întoarce 200 cu date, cea lipsă 404 cu o eroare în JSON, iar DELETE pe o rută doar pentru GET este refuzat cu 405 chiar de router.
Ieșire: program TypeScript și rezultatul capturat
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
type RouteHandler = (
req: IncomingMessage,
res: ServerResponse,
params: Record<string, string>,
) => void
interface Route {
method: string
pattern: string
handler: RouteHandler
}
function matchPath(pattern: string, path: string): Record<string, string> | undefined {
const want = pattern.split("/")
const got = path.split("/")
if (want.length !== got.length) return undefined
const params: Record<string, string> = {}
for (let i = 0; i < want.length; i++) {
if (want[i].startsWith("{") && want[i].endsWith("}")) {
params[want[i].slice(1, -1)] = got[i]
} else if (want[i] !== got[i]) {
return undefined
}
}
return params
}
// Calea s-a potrivit, dar metoda nu: răspunde cu 405 și spune ce este permis.
function router(routes: Route[]) {
return (req: IncomingMessage, res: ServerResponse): void => {
const url = new URL(req.url ?? "/", "http://localhost")
const allowed: string[] = []
for (const route of routes) {
const params = matchPath(route.pattern, url.pathname)
if (params === undefined) continue
if (route.method === req.method) {
route.handler(req, res, params)
return
}
allowed.push(route.method)
}
if (allowed.length > 0) {
res.writeHead(405, { Allow: allowed.join(", ") }).end()
return
}
res.writeHead(404).end()
}
}
const notes = new Map<string, string>([["1", "listă de cumpărături"]])
const routes: Route[] = [
{
method: "GET",
pattern: "/notes/{id}",
handler: (req, res, params) => {
res.setHeader("Content-Type", "application/json")
const text = notes.get(params.id)
if (text === undefined) {
res.writeHead(404).end(JSON.stringify({ error: "notița nu a fost găsită" }))
return
}
res.end(JSON.stringify({ id: params.id, text }))
},
},
]
async function main(): Promise<void> {
const server = createServer(router(routes))
await new Promise<void>(resolve => server.listen(0, () => resolve()))
const base = `http://localhost:${(server.address() as { port: number }).port}`
for (const path of ["/notes/1", "/notes/99"]) {
const res = await fetch(base + path)
console.log("GET ", path.padEnd(9), "->", res.status, await res.text())
}
const refused = await fetch(`${base}/notes/1`, { method: "DELETE" })
console.log("DELETE", "/notes/1".padEnd(9), "->", refused.status, refused.statusText)
server.close()
}
main()