v1.0
Acessar api.vupi.us

Início Rápido — Módulo em 5 minutos

Crie um módulo de Tarefas (To-Do) completo e funcional do zero.

O que você vai criar

Um módulo Tarefa com CRUD completo: listar, criar, buscar, atualizar e deletar tarefas. Com banco de dados, autenticação e rotas funcionando.

Não quer usar Controllers?

Você pode usar closures diretamente nas rotas, sem criar nenhum Controller. O sistema aceita qualquer callable como handler. Veja o Exemplo Completo para o padrão com todas as camadas.

$router->get('/api/ping', function (Request $request): Response {
    return Response::json(['pong' => true]);
});

Criar o projeto na IDE

  1. Acesse /dashboard/ide no menu principal
  2. Clique em "Novo Projeto"
  3. Preencha os campos:
    • Nome do projeto: Módulo de Tarefas
    • Nome do módulo: Tarefa (PascalCase, sem espaços)
  4. Marque "Gerar estrutura padrão automaticamente"
  5. Clique em "Criar Projeto"
Estrutura criada automaticamente

A IDE cria 16 arquivos organizados em pastas: Controllers, Models, Repositories, Routes, Database (Migrations e Seeders), Views e Config. Você só precisa editar 4 arquivos para ter um CRUD completo funcionando.

Editar a Migration (criar a tabela)

Abra o arquivo de migration em Database/Migrations/ (algo como YYYY_MM_DD_HHMMSS_create_tarefas_table.php) no editor e substitua o conteúdo por:

<?php

use PDO;

return [
    'up' => function (PDO $pdo): void {
        $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
        if ($driver === 'pgsql') {
            $pdo->exec("CREATE TABLE IF NOT EXISTS tarefas (
                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
                titulo VARCHAR(255) NOT NULL,
                concluida BOOLEAN NOT NULL DEFAULT FALSE,
                user_id VARCHAR(255) NOT NULL,
                criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
            )");
        } else {
            $pdo->exec("CREATE TABLE IF NOT EXISTS tarefas (
                id CHAR(36) PRIMARY KEY,
                titulo VARCHAR(255) NOT NULL,
                concluida TINYINT(1) NOT NULL DEFAULT 0,
                user_id VARCHAR(255) NOT NULL,
                criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        }
    },
    'down' => function (PDO $pdo): void {
        $pdo->exec("DROP TABLE IF EXISTS tarefas");
    },
];

Editar o Repository (queries SQL)

Abra Repositories/TarefaRepository.php no editor e substitua por:

<?php

namespace Src\Modules\Tarefa\Repositories;

use PDO;

final class TarefaRepository
{
    private string $table = 'tarefas';

    public function __construct(private readonly PDO $pdo) {}

    public function findByUser(string $userId): array
    {
        $stmt = $this->pdo->prepare(
            "SELECT * FROM {$this->table} WHERE user_id = ? ORDER BY criado_em DESC"
        );
        $stmt->execute([$userId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function findById(string $id, string $userId): ?array
    {
        $stmt = $this->pdo->prepare(
            "SELECT * FROM {$this->table} WHERE id = ? AND user_id = ?"
        );
        $stmt->execute([$id, $userId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row !== false ? $row : null;
    }

    public function create(array $data): array
    {
        $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
        $id = $driver === 'pgsql'
            ? $this->pdo->query('SELECT gen_random_uuid()')->fetchColumn()
            : bin2hex(random_bytes(16));

        $this->pdo->prepare(
            "INSERT INTO {$this->table} (id, titulo, user_id) VALUES (?, ?, ?)"
        )->execute([$id, $data['titulo'], $data['user_id']]);

        return $this->findById($id, $data['user_id']) ?? ['id' => $id];
    }

    public function update(string $id, string $userId, array $data): void
    {
        $this->pdo->prepare(
            "UPDATE {$this->table} SET titulo = ?, concluida = ? WHERE id = ? AND user_id = ?"
        )->execute([$data['titulo'], $data['concluida'] ? 1 : 0, $id, $userId]);
    }

    public function delete(string $id, string $userId): void
    {
        $this->pdo->prepare(
            "DELETE FROM {$this->table} WHERE id = ? AND user_id = ?"
        )->execute([$id, $userId]);
    }
}

Editar o Controller (endpoints)

Abra Controllers/TarefaController.php no editor e substitua por:

<?php

namespace Src\Modules\Tarefa\Controllers;

use Src\Kernel\Http\Request\Request;
use Src\Kernel\Http\Response\Response;
use Src\Modules\Tarefa\Repositories\TarefaRepository;

final class TarefaController
{
    public function __construct(
        private readonly TarefaRepository $repository
    ) {}

    public function listar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        return Response::json(['tarefas' => $this->repository->findByUser($userId)]);
    }

    public function criar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        $titulo = trim($request->body['titulo'] ?? '');

        if ($titulo === '') {
            return Response::json(['error' => 'O campo titulo e obrigatorio.'], 422);
        }

        $tarefa = $this->repository->create(['titulo' => $titulo, 'user_id' => $userId]);
        return Response::json(['tarefa' => $tarefa], 201);
    }

    public function buscar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        $tarefa = $this->repository->findById($request->params['id'], $userId);

        if ($tarefa === null) {
            return Response::json(['error' => 'Tarefa nao encontrada.'], 404);
        }
        return Response::json(['tarefa' => $tarefa]);
    }

    public function atualizar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        $id = $request->params['id'];

        if ($this->repository->findById($id, $userId) === null) {
            return Response::json(['error' => 'Tarefa nao encontrada.'], 404);
        }

        $this->repository->update($id, $userId, [
            'titulo'    => trim($request->body['titulo'] ?? ''),
            'concluida' => (bool) ($request->body['concluida'] ?? false),
        ]);
        return Response::json(['updated' => true]);
    }

    public function deletar(Request $request): Response
    {
        $userId = $request->attribute('auth_user')->getUuid()->toString();
        $id = $request->params['id'];

        if ($this->repository->findById($id, $userId) === null) {
            return Response::json(['error' => 'Tarefa nao encontrada.'], 404);
        }

        $this->repository->delete($id, $userId);
        return Response::json(['deleted' => true]);
    }
}

Editar as Rotas

Abra Routes/web.php no editor e substitua por:

<?php

use Src\Modules\Tarefa\Controllers\TarefaController;
use Src\Kernel\Middlewares\AuthHybridMiddleware;

/** @var \Src\Kernel\Contracts\RouterInterface $router */

$auth = [AuthHybridMiddleware::class];

$router->get('/api/tarefa',       [TarefaController::class, 'listar'],    $auth);
$router->post('/api/tarefa',      [TarefaController::class, 'criar'],     $auth);
$router->get('/api/tarefa/{id}',  [TarefaController::class, 'buscar'],    $auth);
$router->put('/api/tarefa/{id}',  [TarefaController::class, 'atualizar'], $auth);
$router->delete('/api/tarefa/{id}', [TarefaController::class, 'deletar'], $auth);

Ativar o módulo

  1. No painel lateral da IDE, clique em "Analisar código"
  2. Aguarde a análise — deve aparecer "✓ Módulo aprovado"
  3. Clique em "Ativar"
O que acontece ao ativar?

O sistema sincroniza os arquivos do projeto (que estão no banco de dados da IDE) para o disco em src/Modules/Tarefa/, registra o módulo como ativo no sistema e recarrega o PHP-FPM para que as rotas sejam reconhecidas imediatamente.

Executar as Migrations

Após ativar o módulo, você precisa executar as migrations manualmente para criar a tabela no banco de dados.

  1. Abra o Terminal da IDE (pressione Ctrl + ` ou clique no ícone do terminal)
  2. Execute o comando de migration:
    php vupi migrate --modules
  3. Você deve ver a mensagem indicando que a migration foi executada com sucesso
Verificar se a tabela foi criada

Você pode verificar se a tabela tarefas foi criada acessando o Gerenciador de Banco de Dados na IDE ou consultando diretamente o banco de dados.

Testar as rotas

Pressione Ctrl + T (ou Cmd + T no Mac) para abrir o API Route Tester integrado.

Autenticação necessária

O API Route Tester não envia cookies automaticamente. Para testar rotas protegidas com AuthHybridMiddleware, você precisa obter um token JWT e configurá-lo na aba Auth → Bearer Token. Sem isso, a rota retorna 401 Unauthorized.

Passo 1 — Obter o token JWT

Primeiro, faça login para obter um token de acesso:

Método: POST
URL:    /api/auth/login
Aba:    Body → JSON
Body:
{
  "login": "seu_email_ou_username",
  "senha": "sua_senha"
}

Resposta esperada:
{
  "status": "success",
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",   ← copie este valor
  "expires_in": 3600,
  "usuario": {
    "uuid": "...",
    "nome_completo": "Seu Nome",
    "username": "seu_username",
    "email": "[email protected]",
    "nivel_acesso": "admin_system"
  }
}
Não tem usuário cadastrado?

Você precisa criar um usuário primeiro. Acesse o sistema e crie uma conta através da interface web, ou peça ao administrador para criar um usuário para você.

Passo 2 — Configurar autenticação no Tester

  1. No API Route Tester, clique na aba "Auth"
  2. Selecione "Bearer Token"
  3. Cole o valor do access_token obtido no passo anterior

Passo 3 — Criar uma tarefa

Método: POST
URL:    /api/tarefa
Aba:    Auth → Bearer Token (já configurado)
Aba:    Body → JSON
Body:
{
  "titulo": "Minha primeira tarefa"
}

Resposta esperada:
{
  "tarefa": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "titulo": "Minha primeira tarefa",
    "concluida": "0",
    "user_id": "...",
    "criado_em": "2026-05-17 10:30:00"
  }
}

Passo 4 — Listar todas as tarefas

Método: GET
URL:    /api/tarefa
Aba:    Auth → Bearer Token (já configurado)

Resposta esperada:
{
  "tarefas": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "titulo": "Minha primeira tarefa",
      "concluida": "0",
      "user_id": "...",
      "criado_em": "2026-05-17 10:30:00"
    }
  ]
}

Passo 5 — Buscar uma tarefa específica

Método: GET
URL:    /api/tarefa/{id}
        (substitua {id} pelo ID real da tarefa)
Aba:    Auth → Bearer Token (já configurado)

Resposta esperada:
{
  "tarefa": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "titulo": "Minha primeira tarefa",
    "concluida": "0",
    "user_id": "...",
    "criado_em": "2026-05-17 10:30:00"
  }
}

Passo 6 — Atualizar uma tarefa

Método: PUT
URL:    /api/tarefa/{id}
Aba:    Auth → Bearer Token (já configurado)
Aba:    Body → JSON
Body:
{
  "titulo": "Minha primeira tarefa (atualizada)",
  "concluida": true
}

Resposta esperada:
{
  "updated": true
}

Passo 7 — Deletar uma tarefa

Método: DELETE
URL:    /api/tarefa/{id}
Aba:    Auth → Bearer Token (já configurado)

Resposta esperada:
{
  "deleted": true
}
Parabéns! 🎉

Você criou um módulo de tarefas completo e funcional com banco de dados, autenticação JWT e CRUD completo. Cada usuário só vê suas próprias tarefas graças ao filtro por user_id.

Possíveis Erros e Soluções

Erro 401 Unauthorized

Causa: Token JWT não configurado ou expirado.
Solução: Faça login novamente para obter um novo token e configure na aba Auth do API Route Tester.

Erro 404 Not Found

Causa: Módulo não foi ativado ou rota está incorreta.
Solução: Verifique se você clicou em "Ativar" após a análise do código. Confirme que a URL está correta (ex: /api/tarefa sem s no final).

Erro 422 Unprocessable Entity

Causa: Campo obrigatório não foi enviado ou está vazio.
Solução: Verifique se o campo titulo está presente no body e não está vazio.

Erro 500 Internal Server Error

Causa: Erro no código PHP ou problema com o banco de dados.
Solução: Abra o Terminal da IDE (Ctrl + `) e execute o arquivo com debug: F6. Verifique os logs de erro para identificar o problema.

Próximos Passos