Makrosites

Suas ideias em realidade digital!

Como criar uma API REST em PHP puro (JSON + Rotas + CRUD + Token Bearer)

Como criar uma API REST em PHP puro (JSON + Rotas + CRUD + Token Bearer)

O que você vai construir neste tutorial

Neste guia você vai montar uma API REST em PHP puro (sem framework) com:

Estrutura de pastas (simples e escalável)

/public
  /api
    index.php
    .htaccess
/src
  /Controllers
    PostController.php
  /Core
    Router.php
    Response.php
    Auth.php
  /Db
    Database.php

1) Ativando rotas amigáveis (Apache / .htaccess)

Crie /public/api/.htaccess:

RewriteEngine On

# se arquivo ou pasta existe, entrega direto
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# tudo vai para o index.php
RewriteRule ^ index.php [QSA,L]

2) index.php: ponto de entrada da API

Crie /public/api/index.php:

<?php
header('Content-Type: application/json; charset=UTF-8');

require_once __DIR__ . '/../..//src/Core/Router.php';
require_once __DIR__ . '/../..//src/Core/Response.php';
require_once __DIR__ . '/../..//src/Core/Auth.php';
require_once __DIR__ . '/../..//src/Controllers/PostController.php';

$router = new Router();

// Health check
$router->get('/api/health', function() {
  Response::json(['ok' => true, 'service' => 'makrosites-api']);
});

// Posts CRUD (exemplo)
$controller = new PostController();

$router->get('/api/posts', [$controller, 'index']);
$router->get('/api/posts/{id}', [$controller, 'show']);

// rotas protegidas
$router->post('/api/posts', function() use ($controller) {
  Auth::requireBearer();
  $controller->store();
});

$router->put('/api/posts/{id}', function($params) use ($controller) {
  Auth::requireBearer();
  $controller->update($params['id']);
});

$router->delete('/api/posts/{id}', function($params) use ($controller) {
  Auth::requireBearer();
  $controller->destroy($params['id']);
});

// executa
$router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);

3) Router.php: roteador simples (com parâmetros)

Crie /src/Core/Router.php:

<?php
class Router {
  private array $routes = [];

  public function get($path, $handler)    { $this->add('GET', $path, $handler); }
  public function post($path, $handler)   { $this->add('POST', $path, $handler); }
  public function put($path, $handler)    { $this->add('PUT', $path, $handler); }
  public function delete($path, $handler) { $this->add('DELETE', $path, $handler); }

  private function add($method, $path, $handler) {
    $this->routes[] = compact('method', 'path', 'handler');
  }

  public function dispatch($method, $uri) {
    $path = parse_url($uri, PHP_URL_PATH);

    foreach ($this->routes as $r) {
      if ($r['method'] !== $method) continue;

      $params = [];
      $pattern = preg_replace('#\{([a-zA-Z_][a-zA-Z0-9_]*)\}#', '(?P<$1>[^/]+)', $r['path']);
      $pattern = '#^' . $pattern . '$#';

      if (preg_match($pattern, $path, $matches)) {
        foreach ($matches as $k => $v) {
          if (!is_int($k)) $params[$k] = $v;
        }

        // handler pode ser callable direto ou closure que recebe params
        if (is_array($r['handler'])) {
          return call_user_func($r['handler'], $params);
        }
        return $r['handler']($params);
      }
    }

    Response::error('Not Found', 404);
  }
}

4) Response.php: JSON padronizado

Crie /src/Core/Response.php:

<?php
class Response {
  public static function json($data, int $code = 200) {
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
  }

  public static function error(string $message, int $code = 400, $details = null) {
    $payload = ['error' => $message];
    if ($details !== null) $payload['details'] = $details;
    self::json($payload, $code);
  }

  public static function readJsonBody(): array {
    $raw = file_get_contents('php://input');
    $data = json_decode($raw, true);
    if (!is_array($data)) self::error('JSON inválido', 422);
    return $data;
  }
}

5) Auth.php: Bearer Token (simples, direto)

Crie /src/Core/Auth.php:

<?php
class Auth {
  public static function requireBearer() {
    $hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

    // alguns servidores usam REDIRECT_HTTP_AUTHORIZATION
    if (!$hdr && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
      $hdr = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
    }

    if (!preg_match('/Bearer\s+(.*)$/i', $hdr, $m)) {
      Response::error('Unauthorized', 401);
    }

    $token = trim($m[1]);
    // TODO: valide no banco
    if ($token !== 'SEU_TOKEN_FIXO_DE_TESTE') {
      Response::error('Token inválido', 401);
    }
  }
}

6) PostController.php: CRUD de exemplo

Crie /src/Controllers/PostController.php:

<?php
require_once __DIR__ . '/../Core/Response.php';

class PostController {

  public function index() {
    // exemplo estático (depois você liga no banco)
    Response::json([
      ['id' => 1, 'title' => 'Post 1'],
      ['id' => 2, 'title' => 'Post 2'],
    ]);
  }

  public function show($params) {
    $id = (int)($params['id'] ?? 0);
    if ($id <= 0) Response::error('ID inválido', 422);

    Response::json(['id' => $id, 'title' => 'Exemplo de post']);
  }

  public function store() {
    $data = Response::readJsonBody();
    if (empty($data['title'])) Response::error('title é obrigatório', 422);

    // aqui você salva no banco...
    Response::json(['message' => 'Criado com sucesso', 'data' => $data], 201);
  }

  public function update($id) {
    $id = (int)$id;
    if ($id <= 0) Response::error('ID inválido', 422);

    $data = Response::readJsonBody();
    Response::json(['message' => 'Atualizado', 'id' => $id, 'data' => $data]);
  }

  public function destroy($id) {
    $id = (int)$id;
    if ($id <= 0) Response::error('ID inválido', 422);

    Response::json(['message' => 'Deletado', 'id' => $id]);
  }
}

7) Testando com curl

GET sem token:

curl -i https://www.makrosites.com.br/api/posts

POST com Bearer Token:

curl -i -X POST https://www.makrosites.com.br/api/posts \
  -H "Authorization: Bearer SEU_TOKEN_FIXO_DE_TESTE" \
  -H "Content-Type: application/json" \
  -d '{"title":"Meu post"}'

Melhorias recomendadas (para produção)