Vai al contenuto

Come creare una REST API in PHP e MySQL — Guida Completa 2026

PHP · REST API · MySQL · JSON

Le REST API sono oggi la spina dorsale di qualsiasi applicazione moderna: le usano le app mobile per comunicare con il server, i frontend React e Vue per ricevere dati, i sistemi di terze parti per integrarsi con il tuo prodotto. Eppure costruirne una in PHP viene spesso percepito come qualcosa di complicato, riservato a chi usa framework come Laravel.

In questa guida vedremo come realizzare una REST API completa in PHP puro — senza framework, zero dipendenze — collegata a un database MySQL, con le quattro operazioni fondamentali (GET, POST, PUT, DELETE) e una struttura pronta per la produzione.

Cosa costruiremo: un'API per gestire un archivio di articoli (titolo, contenuto, autore). Stessa logica applicabile a prodotti, utenti, ordini, prenotazioni — qualsiasi entità tu voglia esporre via API.
1

Cos'è una REST API e perché PHP è ancora ottimo

REST (Representational State Transfer) è un'architettura che definisce come client e server si scambiano dati via HTTP. Le regole sono poche e semplici:

Metodo HTTP Operazione Esempio endpoint Cosa fa
GET Leggi /api/articoli Restituisce la lista degli articoli
GET Leggi uno /api/articoli/5 Restituisce l'articolo con ID 5
POST Crea /api/articoli Crea un nuovo articolo
PUT Modifica /api/articoli/5 Aggiorna l'articolo con ID 5
DELETE Elimina /api/articoli/5 Cancella l'articolo con ID 5

PHP gestisce nativamente tutti questi metodi HTTP, iò JSON con json_encode() e json_decode(), e parla con MySQL tramite PDO. Non hai bisogno di altro per costruire un'API robusta.

2

Struttura del progetto

api/
├── index.php       <-- router principale
├── config.php      <-- credenziali DB
├── Database.php    <-- classe connessione PDO
├── Articolo.php    <-- classe modello
├── .htaccess       <-- riscrittura URL

Ogni file ha un compito preciso: index.php instrada le richieste, Articolo.php contiene la logica dei dati, Database.php gestisce la connessione. Separazione netta, facile da estendere.

3

Database MySQL — crea la tabella

Esegui questo SQL in phpMyAdmin:

CREATE DATABASE IF NOT EXISTS mia_api CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE mia_api;

CREATE TABLE articoli (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    titolo     VARCHAR(255) NOT NULL,
    contenuto  TEXT,
    autore     VARCHAR(100),
    creato_il  DATETIME DEFAULT CURRENT_TIMESTAMP,
    aggiornato DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Dati di esempio
INSERT INTO articoli (titolo, contenuto, autore) VALUES
('Primo articolo', 'Contenuto del primo articolo...', 'Mario Rossi'),
('Secondo articolo', 'Contenuto del secondo articolo...', 'Lucia Bianchi');
SQL
4

config.php e Database.php

<?php
// config.php
define('DB_HOST', 'localhost');
define('DB_NAME', 'mia_api');
define('DB_USER', 'root');       // cambia con il tuo utente
define('DB_PASS', 'password');   // cambia con la tua password
define('DB_CHARSET', 'utf8mb4');
PHP — config.php
<?php
// Database.php
require_once 'config.php';

class Database {

    private static ?PDO $instance = null;

    public static function connect(): PDO {
        if (self::$instance === null) {
            $dsn = 'mysql:host=' . DB_HOST
                 . ';dbname='   . DB_NAME
                 . ';charset='  . DB_CHARSET;

            self::$instance = new PDO($dsn, DB_USER, DB_PASS, [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false,
            ]);
        }
        return self::$instance;
    }
}
PHP — Database.php
💡 Singleton pattern: Database::connect() crea la connessione PDO solo alla prima chiamata e la riusa per tutta la richiesta — zero connessioni duplicate.
5

Articolo.php — il modello

Tutta la logica SQL sta qui. Il controller (index.php) non tocca mai il database direttamente.

<?php
// Articolo.php
require_once 'Database.php';

class Articolo {

    private PDO $db;

    public function __construct() {
        $this->db = Database::connect();
    }

    // GET /articoli — lista tutti
    public function getAll(): array {
        $stmt = $this->db->query(
            'SELECT id, titolo, autore, creato_il FROM articoli ORDER BY creato_il DESC'
        );
        return $stmt->fetchAll();
    }

    // GET /articoli/{id} — leggi uno
    public function getOne(int $id): ?array {
        $stmt = $this->db->prepare(
            'SELECT * FROM articoli WHERE id = :id LIMIT 1'
        );
        $stmt->execute([':id' => $id]);
        $row = $stmt->fetch();
        return $row ?: null;
    }

    // POST /articoli — crea
    public function create(array $data): array {
        $stmt = $this->db->prepare(
            'INSERT INTO articoli (titolo, contenuto, autore)
             VALUES (:titolo, :contenuto, :autore)'
        );
        $stmt->execute([
            ':titolo'    => $data['titolo']    ?? '',
            ':contenuto' => $data['contenuto'] ?? '',
            ':autore'    => $data['autore']    ?? '',
        ]);
        return $this->getOne((int)$this->db->lastInsertId());
    }

    // PUT /articoli/{id} — aggiorna
    public function update(int $id, array $data): ?array {
        $stmt = $this->db->prepare(
            'UPDATE articoli
             SET titolo = :titolo, contenuto = :contenuto, autore = :autore
             WHERE id = :id'
        );
        $stmt->execute([
            ':titolo'    => $data['titolo']    ?? '',
            ':contenuto' => $data['contenuto'] ?? '',
            ':autore'    => $data['autore']    ?? '',
            ':id'        => $id,
        ]);
        return $stmt->rowCount() > 0 ? $this->getOne($id) : null;
    }

    // DELETE /articoli/{id} — elimina
    public function delete(int $id): bool {
        $stmt = $this->db->prepare('DELETE FROM articoli WHERE id = :id');
        $stmt->execute([':id' => $id]);
        return $stmt->rowCount() > 0;
    }
}
PHP — Articolo.php
6

index.php — il router

Questo è il cuore dell'API. Legge il metodo HTTP e l'URL, instrada alla funzione giusta e risponde sempre in JSON.

<?php
// index.php
require_once 'Articolo.php';

// Imposta sempre Content-Type JSON
header('Content-Type: application/json; charset=UTF-8');

// Gestione CORS (per chiamate da altri domini o frontend JS)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

// Pre-flight CORS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit();
}

// Helper: risposta JSON
function risposta(int $code, mixed $data): void {
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    exit();
}

// Parsing URL — estrae /api/articoli e /api/articoli/5
$uri    = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$parti  = explode('/', trim($uri, '/'));
// $parti[0] = 'api', $parti[1] = 'articoli', $parti[2] = ID (opzionale)

$risorsa = $parti[1] ?? '';
$id      = isset($parti[2]) && is_numeric($parti[2]) ? (int)$parti[2] : null;
$metodo  = $_SERVER['REQUEST_METHOD'];

// Body JSON (per POST e PUT)
$body = json_decode(file_get_contents('php://input'), true) ?? [];

// ── Router ───────────────────────────────────────────────────
if ($risorsa !== 'articoli') {
    risposta(404, ['errore' => 'Risorsa non trovata']);
}

$articolo = new Articolo();

try {

    // GET /api/articoli — lista tutti
    if ($metodo === 'GET' && $id === null) {
        $lista = $articolo->getAll();
        risposta(200, ['successo' => true, 'totale' => count($lista), 'dati' => $lista]);
    }

    // GET /api/articoli/{id} — leggi uno
    if ($metodo === 'GET' && $id !== null) {
        $item = $articolo->getOne($id);
        if (!$item) risposta(404, ['errore' => 'Articolo non trovato']);
        risposta(200, ['successo' => true, 'dati' => $item]);
    }

    // POST /api/articoli — crea
    if ($metodo === 'POST') {
        if (empty($body['titolo'])) {
            risposta(422, ['errore' => 'Il campo titolo e\' obbligatorio']);
        }
        $nuovo = $articolo->create($body);
        risposta(201, ['successo' => true, 'messaggio' => 'Articolo creato', 'dati' => $nuovo]);
    }

    // PUT /api/articoli/{id} — aggiorna
    if ($metodo === 'PUT' && $id !== null) {
        $aggiornato = $articolo->update($id, $body);
        if (!$aggiornato) risposta(404, ['errore' => 'Articolo non trovato o nessuna modifica']);
        risposta(200, ['successo' => true, 'messaggio' => 'Articolo aggiornato', 'dati' => $aggiornato]);
    }

    // DELETE /api/articoli/{id} — elimina
    if ($metodo === 'DELETE' && $id !== null) {
        $eliminato = $articolo->delete($id);
        if (!$eliminato) risposta(404, ['errore' => 'Articolo non trovato']);
        risposta(200, ['successo' => true, 'messaggio' => 'Articolo eliminato']);
    }

    risposta(405, ['errore' => 'Metodo non consentito']);

} catch (Exception $e) {
    error_log($e->getMessage());
    risposta(500, ['errore' => 'Errore interno del server']);
}
PHP — index.php
7

.htaccess — URL puliti

Senza questo file gli URL avrebbero index.php nel mezzo. Con la riscrittura Apache, /api/articoli/5 punta sempre a index.php:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
.htaccess
8

Esempio di risposta JSON

Una chiamata GET /api/articoli restituisce:

{
  "successo": true,
  "totale": 2,
  "dati": [
    {
      "id": 1,
      "titolo": "Primo articolo",
      "autore": "Mario Rossi",
      "creato_il": "2025-06-01 10:23:00"
    }
  ]
}

Una chiamata POST /api/articoli con body JSON crea l'articolo e restituisce il record appena inserito con HTTP 201. Un DELETE /api/articoli/5 su un ID inesistente risponde con HTTP 404 e un messaggio chiaro.

9

Testare l'API con Postman o curl

Con curl da terminale

# GET tutti gli articoli
curl https://tuosito.it/api/articoli

# GET articolo singolo
curl https://tuosito.it/api/articoli/1

# POST — crea nuovo articolo
curl -X POST https://tuosito.it/api/articoli \
  -H "Content-Type: application/json" \
  -d '{"titolo":"Nuovo","contenuto":"Testo...","autore":"Anna"}'

# PUT — aggiorna
curl -X PUT https://tuosito.it/api/articoli/1 \
  -H "Content-Type: application/json" \
  -d '{"titolo":"Aggiornato","contenuto":"Nuovo testo","autore":"Anna"}'

# DELETE
curl -X DELETE https://tuosito.it/api/articoli/1
BASH — curl
💡 Postman: scarica Postman gratuitamente — ha un'interfaccia grafica comoda per testare tutti i metodi HTTP senza scrivere comandi.
10

Sicurezza: le basi prima di andare in produzione

  • Usa sempre prepared statements PDO (come nell'esempio) — protezione nativa da SQL injection
  • Aggiungi autenticazione API Key o JWT negli header per le operazioni di scrittura
  • Valida e sanifica ogni campo prima di scrivere nel DB
  • Imposta CORS restrittivo in produzione — sostituisci * con il dominio del tuo frontend
  • Usa HTTPS sempre — senza TLS le chiamate viaggiano in chiaro
  • Logga gli errori su file con error_log(), mai mostrarli all'utente finale
  • Aggiungi rate limiting per IP per evitare abusi dell'endpoint
  • Considera un campo api_key nella tabella utenti per autenticare i client

Esempio autenticazione API Key minimalista

// Aggiungere in index.php, subito dopo gli header

$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';

$keysValide = ['chiave-segreta-1', 'chiave-segreta-2']; // meglio da DB

if ($metodo !== 'GET' && !in_array($apiKey, $keysValide, true)) {
    risposta(401, ['errore' => 'Chiave API non valida o assente']);
}
// Le letture GET restano pubbliche, scritture richiedono la chiave
PHP — auth semplice

Conclusioni

Hai appena costruito una REST API completa in PHP puro — routing, CRUD, JSON, PDO, gestione errori e CORS — senza installare nemmeno un pacchetto Composer. La struttura che hai visto è già quella usata in produzione da centinaia di progetti reali.

Da qui puoi estendere aggiungendo autenticazione JWT, paginazione sui risultati, endpoint di ricerca con FULLTEXT MySQL, o una seconda risorsa (es. /api/utenti) seguendo esattamente lo stesso schema.

🚀 Prossimi step: aggiungi JWT per autenticazione stateless, implementa la paginazione con LIMIT e OFFSET, oppure integra questa API come backend per un'app React o Vue — è già pronta per farlo.
Vuoi maggiori informazioni? Chatta con noi
x-lab soft
Rispondiamo entro un giorno lavorativo