Guida alla creazione e al test di API REST con PHP | Prof. Giagnotti
PROF GIAGNOTTI · TORNA A → · GUIDE
🗺 GUIDE API · PHP · WEB SERVICE REST
A cura del Prof. Giagnotti · Docente di Informatica e Sistemi e Reti

Guida alla creazione e al test di API REST con PHP

Dal database MySQL alla tua prima API REST completa in PHP: due approcci (procedurale e OOP), sicurezza con prepared statement, CORS e test reali con Thunder Client e Postman.

📦 Scarica ZIP progetto PHP ⏱️ 18 min di lettura 🟡 Livello: Intermedio 🛠️ XAMPP + MySQL + PHP 8
🎯 Obiettivi della guida
✓Comprendere i principi REST: stateless, metodi HTTP, risorse e JSON come formato standard.
✓Creare il database rubrica e la tabella utenti con struttura realistica e sicura.
✓Implementare un Web Service REST procedurale con connessione.php, header.php e rubrica.php.
✓Rifattorizzare il servizio in OOP con classe Database e Rubrica usando prepared statement.
✓Testare tutti i metodi CRUD con Thunder Client / Postman e gestire CORS, errori e validazione.

1. I Web Services REST — perché li usiamo ovunque

📌 Definizione da ricordare

REST (Representational State Transfer) è un'architettura per sistemi distribuiti: ogni risorsa è un URL, ogni azione è un metodo HTTP, ogni risposta è JSON.

I Web Services REST permettono la comunicazione tra client e server tramite HTTP usando metodi standard:

  • GET: recupera dati
  • POST: inserisce dati
  • PUT: aggiorna dati (sostituzione completa o parziale)
  • DELETE: elimina dati

Un servizio REST espone API che lavorano su dati in JSON (o XML). È lo standard per collegare app React, Vue, mobile app e qualsiasi frontend al backend.

Principi fondamentali

01 · ARCHITETTURA

Separazione Client/Server

Frontend e backend evolvono indipendentemente. Il frontend consuma solo JSON.

02 · STATELESS

Ogni richiesta è indipendente

Il server non ricorda le richieste precedenti. Tutto il necessario è nell'HTTP request.

03 · ACCESSIBILITÀ

Accessibile via URL

Ogni risorsa ha un endpoint: /utenti, /utenti/5. Semplice da capire e testare.

🎯 Esempio in classe

Quando fate una app in React per la rubrica della scuola, il frontend fa fetch('/rubrica.php'). Non sa se dietro c'è MySQL, file JSON o Firebase. L'API REST è il contratto: “se fai GET ottieni JSON con records”. Questo disaccoppiamento è quello che vi chiedo all'esame di TPSIT.

CLIENT React / Thunder Postman GET POST PUT DELETE JSON {records: [...]} API REST PHP rubrica.php header CORS + CRUD MySQL rubrica.utenti id, cognome, nome...

2. Il progetto rubrica — cosa costruiremo

Realizziamo un servizio WEB con API REST in PHP che interagisce con una tabella utenti attraverso CRUD completo. Ogni utente ha: telefono, cognome, nome, email, via, città, provincia, CAP.

💬 Tip del docente

In classe parto sempre da una rubrica perché è semplice ma completa: ha tutti i tipi di dati che vi servono (stringhe, email, CAP) e vi permette di capire validazione e sanificazione. Se capite la rubrica, potete fare qualsiasi gestionale.

3. Creazione del database MySQL

Apriamo XAMPP, avviamo Apache e MySQL, poi phpMyAdmin cliccando su “Admin” in corrispondenza di MySQL. Creiamo il database rubrica e la tabella utenti.

SQL · Creazione DB e tabella
CREATE DATABASE rubrica; USE rubrica; CREATE TABLE utenti( id INT AUTO_INCREMENT PRIMARY KEY, cognome VARCHAR(50) NOT NULL, nome VARCHAR(50) NOT NULL, email VARCHAR(100), via VARCHAR(100), citta VARCHAR(50), provincia VARCHAR(2), cap VARCHAR(10), telefono VARCHAR(20), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

Popoliamo la tabella con dati di test realistici:

SQL · Dati di test
INSERT INTO utenti (cognome, nome, email, via, citta, provincia, cap, telefono) VALUES ('Rossi', 'Mario', 'mario.rossi@example.com', 'Via Roma 12', 'Milano', 'MI', '20121', '3201234567'), ('Verdi', 'Luca', 'luca.verdi@example.com', 'Via Torino 45', 'Torino', 'TO', '10121', '3339876543'), ('Bianchi', 'Giulia', 'giulia.bianchi@example.com', 'Viale Italia 8', 'Firenze', 'FI', '50100', '3471112233'), ('Russo', 'Anna', 'anna.russo@example.com', 'Corso Venezia 5', 'Napoli', 'NA', '80100', '3894455667'), ('Gallo', 'Stefano', 'stefano.gallo@example.com', 'Via Garibaldi 22', 'Genova', 'GE', '16100', '3925544332'), ('Ferrari', 'Elena', 'elena.ferrari@example.com', 'Via Dante 14', 'Bologna', 'BO', '40121', '3406677889');
⚠️ Errore comune

Dimenticare AUTO_INCREMENT sull'id. Senza, la POST fallisce perché MySQL non sa quale id assegnare. E sempre NOT NULL su cognome e nome: sono i due campi che renderemo obbligatori anche in PHP.

4. Approccio procedurale — connessione.php

Creiamo la cartella WebServicePHP_procedurale dentro htdocs di XAMPP. Apriamola in VS Code. Tutti i file andranno qui.

PHP · connessione.php - connessione OOP a MySQLi
<?php $servername = "localhost"; $username = "root"; $password = ""; $database = "rubrica"; // Connessione in stile OOP $conn = new mysqli($servername, $username, $password, $database); // Verifica della connessione if ($conn->connect_error) { die(json_encode(["message" => "Connessione fallita: " . $conn->connect_error])); } ?>
🎯 Perché json_encode nell'errore?

Standardizzare la risposta in JSON è best practice REST. Se usi die("Errore") con testo puro, Thunder Client e Postman mostrano “non-JSON response”. Con json_encode il client sa sempre cosa aspettarsi, anche in caso di errore. All'interrogazione vi chiedo: perché non restituiamo HTML in un'API?

5. Gli header CORS — perché servono davvero

Nella stessa cartella creiamo header.php con gli header che richiameremo in tutti gli script CRUD.

PHP · header.php - Configurazione CORS completa
<?php // Accesso da qualsiasi dominio header("Access-Control-Allow-Origin: *"); // Risposta in formato JSON header("Content-Type: application/json; charset=UTF-8"); // Permette metodi PUT, GET, POST, DELETE header("Access-Control-Allow-Methods: PUT, GET, POST, DELETE"); // Tempo di caching per le preflight request header("Access-Control-Max-Age: 3600"); // Header consentiti header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With"); ?>
CORS · ORIGIN

Access-Control-Allow-Origin: *

Rende l'API accessibile da qualsiasi dominio. In produzione limita a domini specifici.

CORS · MAX-AGE

Access-Control-Max-Age: 3600

Il browser memorizza la risposta preflight per 1 ora, evitando OPTIONS ripetute.

CORS · HEADERS

Allow-Headers

Content-Type per JSON, Authorization per token JWT, X-Requested-With per AJAX.

💬 Tip del docente

Spiego sempre la preflight: quando fai PUT o DELETE con header custom, il browser fa prima una richiesta OPTIONS per chiedere “posso?”. Con Max-Age: 3600 dici “sì, per 1 ora non chiedere più”. Riduce traffico e latenza. Se in Thunder Client funziona ma nel browser no, il problema è 90% CORS.

6. Logica CRUD in rubrica.php (procedurale)

Ora creiamo rubrica.php che contiene la logica per i 4 metodi. Prima includiamo connessione, header e leggiamo il metodo e il body.

PHP · rubrica.php - Bootstrap
<?php // Header comuni include 'connessione.php'; include_once 'header.php'; // Ottieni il metodo HTTP $method = $_SERVER['REQUEST_METHOD']; // Leggi il corpo della richiesta $input = json_decode(file_get_contents("php://input"), true);
📌 Da ricordare: php://input

file_get_contents("php://input") legge il raw body della richiesta HTTP. Per API JSON è indispensabile: $_POST funziona solo con form-urlencoded, non con JSON.

Switch-case per il routing

PHP · Case GET - Lettura completa
switch ($method) { case 'GET': $sql = "SELECT * FROM utenti"; $result = mysqli_query($conn, $sql); $rubrica = []; while ($row = mysqli_fetch_assoc($result)) { $rubrica[] = $row; } echo json_encode(["records" => $rubrica]); break;
PHP · Case POST - Inserimento con sanificazione
case 'POST': if (!empty($input['cognome']) && !empty($input['nome'])) { $cognome = mysqli_real_escape_string($conn, $input['cognome']); $nome = mysqli_real_escape_string($conn, $input['nome']); $email = mysqli_real_escape_string($conn, $input['email'] ?? ''); $via = mysqli_real_escape_string($conn, $input['via'] ?? ''); $citta = mysqli_real_escape_string($conn, $input['citta'] ?? ''); $provincia = mysqli_real_escape_string($conn, $input['provincia'] ?? ''); $cap = mysqli_real_escape_string($conn, $input['cap'] ?? ''); $telefono = mysqli_real_escape_string($conn, $input['telefono'] ?? ''); $sql = "INSERT INTO utenti (cognome, nome, email, via, citta, provincia, cap, telefono) VALUES ('$cognome', '$nome', '$email', '$via', '$citta', '$provincia', '$cap', '$telefono')"; if (mysqli_query($conn, $sql)) { echo json_encode(["message" => "Contatto inserito con successo."]); } else { echo json_encode(["message" => "Errore durante l'inserimento."]); } } else { echo json_encode(["message" => "Dati incompleti: cognome e nome obbligatori."]); } break;
🎯 Perché mysqli_real_escape_string e intval?

mysqli_real_escape_string() protegge da SQL Injection escapando caratteri pericolosi come apici. intval() converte in intero qualsiasi input per l'id, bloccando tentativi tipo 1 OR 1=1. In classe mostro sempre un attacco demo: senza sanificazione, un utente malintenzionato cancella tutta la tabella.

PHP · Case PUT - Aggiornamento dinamico con implode
case 'PUT': if (!empty($input['id'])) { $id = intval($input['id']); $updates = []; foreach (['cognome','nome','email','via','citta','provincia','cap','telefono'] as $field) { if (isset($input[$field])) { $value = mysqli_real_escape_string($conn, $input[$field]); $updates[] = "$field = '$value'"; } } if (!empty($updates)) { $sql = "UPDATE utenti SET " . implode(", ", $updates) . " WHERE id = $id"; if (mysqli_query($conn, $sql)) { echo json_encode(["message" => "Contatto aggiornato con successo."]); } else { echo json_encode(["message" => "Errore durante l'aggiornamento."]); } } else { echo json_encode(["message" => "Nessun campo da aggiornare."]); } } else { echo json_encode(["message" => "ID non fornito."]); } break;

implode(", ", $updates) unisce l'array in stringa SQL tipo cognome='Giagnotti', nome='Mario'. È elegante perché aggiorna solo i campi inviati.

PHP · Case DELETE - Eliminazione
case 'DELETE': if (!empty($input['id'])) { $id = intval($input['id']); $sql = "DELETE FROM utenti WHERE id = $id"; if (mysqli_query($conn, $sql)) { echo json_encode(["message" => "Contatto eliminato con successo."]); } else { echo json_encode(["message" => "Errore durante l'eliminazione."]); } } else { echo json_encode(["message" => "ID non fornito."]); } break; default: http_response_code(405); echo json_encode(["message" => "Metodo non consentito."]); break; } ?>

7. Approccio OOP — classi Database e Rubrica

Ora rifacciamo lo stesso progetto in OOP: più pulito, riutilizzabile e sicuro con prepared statement. Creiamo cartella WebServicePHP_OOP.

connessione.php come classe Database

PHP · Classe Database con getConnection()
<?php class Database { private $servername = "localhost"; private $username = "root"; private $password = ""; private $database = "rubrica"; private $conn; public function getConnection() { $this->conn = new mysqli($this->servername, $this->username, $this->password, $this->database); if ($this->conn->connect_error) { die(json_encode(["message" => "Connessione fallita: " . $this->conn->connect_error])); } return $this->conn; } } ?>
💬 Tip del docente

Perché una classe Database? Perché domani cambi password o passi a un altro host e modifichi un solo file. In più, puoi riutilizzarla per altri progetti. OOP = meno copia-incolla, più manutenibilità. Ve lo chiedo sempre all'orale: vantaggio dell'incapsulamento.

rubrica.php — classe Rubrica + routing

PHP · Classe Rubrica - proprietà e costruttore
<?php include_once 'header.php'; include_once 'connessione.php'; class Rubrica { private $conn; private $table_name = "utenti"; public $id, $cognome, $nome, $email, $via, $citta, $provincia, $cap, $telefono; public function __construct($db) { $this->conn = $db; }
PHP · Metodi CRUD OOP con prepared statement
public function read() { $query = "SELECT * FROM " . $this->table_name; return $this->conn->query($query); } public function create() { $query = "INSERT INTO " . $this->table_name . " (cognome, nome, email, via, citta, provincia, cap, telefono) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; $stmt = $this->conn->prepare($query); $stmt->bind_param("ssssssss", $this->cognome, $this->nome, $this->email, $this->via, $this->citta, $this->provincia, $this->cap, $this->telefono); return $stmt->execute(); } public function update() { $query = "UPDATE " . $this->table_name . " SET cognome=?, nome=?, email=?, via=?, citta=?, provincia=?, cap=?, telefono=? WHERE id=?"; $stmt = $this->conn->prepare($query); $stmt->bind_param("ssssssssi", $this->cognome, $this->nome, $this->email, $this->via, $this->citta, $this->provincia, $this->cap, $this->telefono, $this->id); return $stmt->execute(); } public function delete() { $query = "DELETE FROM " . $this->table_name . " WHERE id=?"; $stmt = $this->conn->prepare($query); $stmt->bind_param("i", $this->id); return $stmt->execute(); } }
🎯 Perché prepared statement?

Con prepare + bind_param non serve più real_escape_string: i dati e la query viaggiano separati, SQL Injection impossibile. È lo standard moderno. La “s” in ssssssssi significa string, “i” intero. Se sbagli l'ordine, l'update fallisce silenziosamente — errore tipico in verifica.

PHP · Istanza e routing REST OOP
// Creazione connessione $database = new Database(); $db = $database->getConnection(); // Istanza Rubrica $rubrica = new Rubrica($db); $method = $_SERVER['REQUEST_METHOD']; $data = json_decode(file_get_contents("php://input"), true); // Routing switch ($method) { case 'GET': $stmt = $rubrica->read(); if ($stmt->num_rows > 0) { $utenti = ["records" => []]; while ($row = $stmt->fetch_assoc()) { $utenti["records"][] = $row; } echo json_encode($utenti); } else { echo json_encode(["message" => "Nessun utente trovato."]); } break; case 'POST': if (!empty($data["cognome"]) && !empty($data["nome"])) { $rubrica->cognome = $data["cognome"]; $rubrica->nome = $data["nome"]; $rubrica->email = $data["email"] ?? ''; // ... altri campi if ($rubrica->create()) { echo json_encode(["message" => "Utente inserito con successo."]); } } break; // PUT, DELETE analoghi... }

8. Test CRUD con Thunder Client e Postman

Per testare il servizio abbiamo due tool: Thunder Client (estensione VS Code, leggera, senza account) e Postman (più completo, da installare da postman.com/downloads).

OperazioneMetodoEndpoint esempioBody
LetturaGETlocalhost/test/WebServicePHP_procedurale/rubrica.phpNessuno
CreazionePOSTstesso endpointJSON con cognome, nome, email...
AggiornamentoPUTstesso endpointJSON con id + campi da modificare
EliminazioneDELETEstesso endpointJSON con id

GET — Lettura

HTTP · GET lista utenti
GET http://localhost/test/WebServicePHP_procedurale/rubrica.php // Risposta: { "records": [ { "id": 1, "cognome": "Rossi", "nome": "Mario", "email": "mario.rossi@example.com", ... }, ... ] }

Clicca l'icona Thunder in VS Code → New Request → GET → incolla endpoint → SEND.

POST — Creazione

JSON · Body POST nuovo utente
{ "cognome": "Test", "nome": "Test", "email": "test@example.com", "via": "Via Test 1", "citta": "TestCity", "provincia": "TT", "cap": "12345", "telefono": "3330000000" }
⚠️ Errore comune in Thunder Client

Dimenticare di selezionare JSON come tipo di body e non Form. Se selezioni Form, php://input rimane vuoto e ottieni “Dati incompleti”. In Thunder Client scegli Body → JSON. In Postman scegli Body → raw → JSON.

PUT — Aggiornamento

JSON · Body PUT modifica
{ "id": "14", "cognome": "Giagnotti", "nome": "Mario" }

DELETE — Eliminazione

JSON · Body DELETE
{ "id": "14" }
💬 Tip del docente

In verifica vi chiedo di testare prima in GET, poi POST, poi PUT con lo stesso id appena creato, poi DELETE. Se fate DELETE di un id inesistente, il mio codice restituisce comunque successo perché MySQL non considera errore una DELETE su 0 righe. Per renderla più robusta, controlla $stmt->affected_rows.

9. Verifica pratica

🔬 Attività — API Rubrica per la scuola

Costruisci l'API completa in entrambe le versioni.

  1. Crea DB rubrica e popola con almeno 6 utenti realistici della tua classe.
  2. Implementa versione procedurale in WebServicePHP_procedurale: connessione, header, rubrica.php con CRUD e sanificazione.
  3. Testa con Thunder Client tutte le operazioni e fai screenshot di GET con records e di POST con messaggio di successo.
  4. Rifattorizza in OOP in WebServicePHP_OOP con prepared statement e confronta la sicurezza.
  5. Aggiungi validazione email con filter_var($email, FILTER_VALIDATE_EMAIL) e restituisci errore JSON se non valida.
✅ Soluzione ragionata

GET deve restituire {"records":[...]}. Se vedi array vuoto, controlla che XAMPP MySQL sia attivo e che il DB sia rubrica. POST deve dare 200 con “Contatto inserito”. Se ottieni “Dati incompleti”, hai dimenticato cognome/nome nel body JSON. PUT senza id → “ID non fornito”. DELETE con id inesistente → comunque “eliminato”, perché la query è tecnicamente riuscita.

Valutazione: 3 punti DB, 4 punti procedurale con header CORS e sanificazione, 4 punti OOP con prepared statement, 3 punti test completi con screenshot, 2 punti bonus per validazione email e controllo affected_rows.

✓REST: risorse = URL, azioni = metodi HTTP (GET, POST, PUT, DELETE), formato = JSON, stateless.
✓header.php: CORS con Allow-Origin, Content-Type JSON, Allow-Methods e Allow-Headers per preflight.
✓php://input + json_decode legge il body JSON, $_POST non funziona con JSON.
✓Procedurale: mysqli_query + mysqli_real_escape_string e intval per sanificare.
✓OOP: classe Database + Rubrica con prepared statement prepare + bind_param anti SQL Injection.
✓Test: Thunder Client / Postman, body JSON raw, verifica codici e messaggi JSON standardizzati.

Lascia un commento