🎯 CW Gravatas API

Documentação da API REST ● Online

📋 Informações Gerais

Base URL

https://api.cwgravatas.com/api

Autenticação

  • A API usa Laravel Sanctum para autenticação
  • Rotas protegidas requerem token no header: Authorization: Bearer {token}
  • Rotas de admin requerem role admin

🔐 Autenticação

POST /api/login Público
Realiza login e retorna token de acesso
📄 Ver Payload e Response
📤 Request Body
{
  "email": "admin@example.com",
  "password": "senha123"
}
📥 Response 200 - Success
{
  "message": "Login realizado com sucesso",
  "user": {
    "id": 1,
    "name": "Admin User",
    "email": "admin@example.com",
    "role": "admin"
  },
  "token": "1|abc123def456..."
}
📥 Response 401 - Unauthorized
{
  "message": "Credenciais inválidas"
}
POST /api/logout Protegido
Revoga o token atual
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
{
  "message": "Logout realizado com sucesso"
}
GET /api/me Protegido
Retorna dados do usuário autenticado
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
{
  "id": 1,
  "name": "Admin User",
  "email": "admin@example.com",
  "role": "admin",
  "created_at": "2026-01-30T10:00:00.000000Z",
  "updated_at": "2026-01-30T10:00:00.000000Z"
}

🏠 Home

GET /api/home Público
Retorna todos os dados necessários para a página inicial (Instagram Links, Tipos de Produtos e Carrossel)
📄 Ver Response
📋 Query Parameters (Opcionais)
screen_size: mobile | tablet | desktop
  - Filtra as imagens do carrossel por tamanho de tela
  - Exemplo: /api/home?screen_size=mobile
📥 Response 200 - Success
{
  "success": true,
  "message": "Dados da home recuperados com sucesso",
  "data": {
    "instagram_links": [
      {
        "id": 1,
        "label": "Instagram Oficial",
        "link": "https://instagram.com/cwgravatas",
        "created_at": "2026-01-30T10:00:00.000000Z",
        "updated_at": "2026-01-30T10:00:00.000000Z"
      },
      {
        "id": 2,
        "label": "Promoções",
        "link": "https://instagram.com/cwgravatas/promocoes",
        "created_at": "2026-01-30T11:00:00.000000Z",
        "updated_at": "2026-01-30T11:00:00.000000Z"
      }
    ],
    "product_types": [
      {
        "id": 1,
        "label": "Gravata",
        "created_at": "2026-01-30T10:00:00.000000Z",
        "updated_at": "2026-01-30T10:00:00.000000Z"
      },
      {
        "id": 2,
        "label": "Suspensório",
        "created_at": "2026-01-30T10:05:00.000000Z",
        "updated_at": "2026-01-30T10:05:00.000000Z"
      }
    ],
    "carousel_images": [
      {
        "id": 1,
        "screen_size": "desktop",
        "cloudinary_public_id": "home-carousel/abc123",
        "url": "https://res.cloudinary.com/.../home-carousel/abc123.jpg",
        "url_thumbnail": "https://res.cloudinary.com/.../home-carousel/abc123_thumb.jpg",
        "ordem": 1,
        "ativa": true,
        "created_at": "2026-01-30T12:00:00.000000Z",
        "updated_at": "2026-01-30T12:00:00.000000Z"
      },
      {
        "id": 2,
        "screen_size": "desktop",
        "cloudinary_public_id": "home-carousel/def456",
        "url": "https://res.cloudinary.com/.../home-carousel/def456.jpg",
        "url_thumbnail": "https://res.cloudinary.com/.../home-carousel/def456_thumb.jpg",
        "ordem": 2,
        "ativa": true,
        "created_at": "2026-01-30T12:05:00.000000Z",
        "updated_at": "2026-01-30T12:05:00.000000Z"
      }
    ]
  }
}
💡 Observações
• Resposta cacheada por 30 minutos
• Retorna apenas imagens ativas do carrossel
• Tipos de produtos são ordenados por label
• Rate limit: 60 requisições por minuto

🛍️ Produtos

Rotas Públicas

GET /api/produtos Público
Lista produtos ativos (para o site público)
📄 Ver Response
📥 Response 200 - Success
[
  {
    "id": 1,
    "imagem": "https://res.cloudinary.com/.../gravata1.jpg",
    "imagens": [
      {
        "id": 1,
        "url": "https://res.cloudinary.com/.../gravata1.jpg",
        "url_thumbnail": "https://res.cloudinary.com/.../thumb_gravata1.jpg",
        "ordem": 0,
        "largura": 800,
        "altura": 600,
        "tamanho_bytes": 245678
      }
    ],
    "nome": "Gravata Slim Azul Marinho",
    "preco": 89.90,
    "descricao": "Gravata slim em poliéster de alta qualidade",
    "tamanho": "Slim",
    "tecido": "Poliéster",
    "cor": "Azul Marinho",
    "estampa": "Lisa",
    "textura": "Acetinada",
    "largura": "5cm",
    "comprimento": "145cm",
    "observacao": null,
    "status": true,
    "quantidade": 50
  }
]
GET /api/produtos/{id} Público
Mostra um produto específico
📄 Ver Response
📥 Response 200 - Success
{
  "id": 1,
  "imagem": "https://res.cloudinary.com/.../gravata1.jpg",
  "imagens": [
    {
      "id": 1,
      "url": "https://res.cloudinary.com/.../gravata1.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb_gravata1.jpg",
      "ordem": 0,
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 245678
    }
  ],
  "nome": "Gravata Slim Azul Marinho",
  "preco": 89.90,
  "descricao": "Gravata slim em poliéster de alta qualidade",
  "tamanho": "Slim",
  "tecido": "Poliéster",
  "cor": "Azul Marinho",
  "estampa": "Lisa",
  "textura": "Acetinada",
  "largura": "5cm",
  "comprimento": "145cm",
  "observacao": null,
  "status": true,
  "quantidade": 50
}
📥 Response 404 - Not Found
{
  "message": "Produto não encontrado"
}

Rotas de Administração

GET /api/produtos/todos Admin
Lista TODOS os produtos (incluindo inativos)
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success (Admin)
[
  {
    "id": 1,
    "imagem": "https://res.cloudinary.com/.../gravata1.jpg",
    "imagens": [...],
    "nome": "Gravata Slim Azul Marinho",
    "preco": 89.90,
    "preco_compra": 45.00,  // ⚠️ Campo visível apenas para admins
    "descricao": "Gravata slim em poliéster",
    "status": true,
    "quantidade": 50,
    ...
  },
  {
    "id": 2,
    "nome": "Gravata Clássica Preta",
    "preco": 79.90,
    "preco_compra": 40.00,
    "status": false,  // ⚠️ Inativo - visível apenas nesta rota
    "quantidade": 0,
    ...
  }
]
POST /api/produtos Admin
Cria um novo produto
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: application/json
📤 Request Body
{
  "nome": "Gravata Slim Azul Marinho",
  "preco": 89.90,
  "preco_compra": 45.00,
  "descricao": "Gravata slim em poliéster de alta qualidade",
  "tamanho": "Slim",
  "tecido": "Poliéster",
  "cor": "Azul Marinho",
  "estampa": "Lisa",
  "textura": "Acetinada",
  "largura": "5cm",
  "comprimento": "145cm",
  "observacao": "Produto importado",
  "status": true,
  "quantidade": 50,
  "imagens": [  // Opcional: imagens já uploadadas
    {
      "cloudinary_public_id": "produtos/abc123",
      "url": "https://res.cloudinary.com/.../gravata1.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb.jpg",
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 245678
    }
  ]
}
📥 Response 201 - Created
{
  "id": 1,
  "imagem": "https://res.cloudinary.com/.../gravata1.jpg",
  "imagens": [...],
  "nome": "Gravata Slim Azul Marinho",
  "preco": 89.90,
  "preco_compra": 45.00,
  "descricao": "Gravata slim em poliéster de alta qualidade",
  "status": true,
  "quantidade": 50,
  ...
}
PUT /api/produtos/{id} Admin
Atualiza um produto
📄 Ver Payload e Response
📤 Request Body (Campos opcionais)
{
  "nome": "Gravata Slim Azul Marinho Atualizada",
  "preco": 99.90,
  "status": false,
  "quantidade": 30
  // Envie apenas os campos que deseja atualizar
}
📥 Response 200 - Success
{
  "id": 1,
  "nome": "Gravata Slim Azul Marinho Atualizada",
  "preco": 99.90,
  "status": false,
  "quantidade": 30,
  ...
}
DELETE /api/produtos/{id} Admin
Remove um produto
📄 Ver Response
📥 Response 200 - Success
{
  "message": "Produto removido com sucesso"
}
PATCH /api/produtos/{id}/estoque Admin
Atualiza o estoque (entrada/saída)
📄 Ver Payload e Response
📤 Request Body
{
  "quantidade": 10  // Use número positivo para entrada, negativo para saída
}
📥 Response 200 - Success
{
  "message": "Estoque atualizado",
  "produto": {
    "id": 1,
    "nome": "Gravata Slim Azul Marinho",
    "quantidade": 60,  // estoque anterior 50 + 10
    ...
  }
}
📥 Response 400 - Estoque Insuficiente
{
  "message": "Estoque insuficiente",
  "estoque_atual": 5
}

Gestão de Imagens

GET /api/produtos/{id}/imagens Admin
Lista imagens do produto
📄 Ver Response
📥 Response 200 - Success
{
  "imagens": [
    {
      "id": 1,
      "url": "https://res.cloudinary.com/.../gravata1.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb_gravata1.jpg",
      "ordem": 0,
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 245678
    },
    {
      "id": 2,
      "url": "https://res.cloudinary.com/.../gravata2.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb_gravata2.jpg",
      "ordem": 1,
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 198234
    }
  ],
  "total": 2,
  "limite": 5
}
POST /api/produtos/{id}/imagens Admin
Upload múltiplas imagens (até 5)
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: multipart/form-data
📤 Request Body (multipart/form-data)
imagens[]: [File] (jpg, jpeg, png, webp - max 2MB cada)
imagens[]: [File]
imagens[]: [File]
// Até 5 imagens por envio
📥 Response 201 - Created
{
  "message": "3 imagem(ns) enviada(s) com sucesso",
  "imagens": [
    {
      "id": 3,
      "url": "https://res.cloudinary.com/.../new1.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb_new1.jpg",
      "ordem": 2,
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 223456
    }
  ],
  "total_imagens": 5
}
📥 Response 422 - Limite Excedido
{
  "message": "Limite de 5 imagens por produto excedido.",
  "imagens_atuais": 4,
  "maximo_permitido": 1
}
DELETE /api/produtos/{id}/imagens/{imagemId} Admin
Remove uma imagem
📄 Ver Response
📥 Response 200 - Success
{
  "message": "Imagem removida com sucesso",
  "total_imagens": 4
}
PUT /api/produtos/{id}/imagens/reordenar Admin
Reordena imagens
📄 Ver Payload e Response
📤 Request Body
{
  "ordem": [3, 1, 2, 5, 4]  // Array de IDs na nova ordem
}
📥 Response 200 - Success
{
  "message": "Ordem das imagens atualizada",
  "imagens": [
    {
      "id": 3,
      "ordem": 0,  // Primeira posição - imagem principal
      ...
    },
    {
      "id": 1,
      "ordem": 1,
      ...
    }
  ]
}
PUT /api/produtos/{id}/imagens/{imagemId}/principal Admin
Define imagem principal
📄 Ver Response
📥 Response 200 - Success
{
  "message": "Imagem principal definida com sucesso",
  "imagem_principal": {
    "id": 3,
    "url": "https://res.cloudinary.com/.../gravata3.jpg",
    "ordem": 0,
    ...
  }
}

🏷️ Tipos de Produtos

Rotas Públicas

GET /api/product-types Público
Lista todos os tipos de produtos
📄 Ver Response
📥 Response 200 - Success
[
  {
    "id": 1,
    "label": "Gravata",
    "created_at": "2026-01-30T10:00:00.000000Z",
    "updated_at": "2026-01-30T10:00:00.000000Z"
  },
  {
    "id": 2,
    "label": "Gravata Borboleta",
    "created_at": "2026-01-30T10:05:00.000000Z",
    "updated_at": "2026-01-30T10:05:00.000000Z"
  },
  {
    "id": 3,
    "label": "Suspensório",
    "created_at": "2026-01-30T10:10:00.000000Z",
    "updated_at": "2026-01-30T10:10:00.000000Z"
  }
]
GET /api/product-types/{id} Público
Mostra um tipo de produto específico
📄 Ver Response
📥 Response 200 - Success
{
  "id": 1,
  "label": "Gravata",
  "created_at": "2026-01-30T10:00:00.000000Z",
  "updated_at": "2026-01-30T10:00:00.000000Z"
}
📥 Response 404 - Not Found
{
  "message": "Tipo de produto não encontrado"
}

Rotas de Administração

POST /api/product-types Admin
Cria um novo tipo de produto
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: application/json
📤 Request Body
{
  "label": "Lenço de Bolso"
}
📥 Response 201 - Created
{
  "id": 4,
  "label": "Lenço de Bolso",
  "created_at": "2026-01-30T15:30:00.000000Z",
  "updated_at": "2026-01-30T15:30:00.000000Z"
}
📥 Response 422 - Validation Error
{
  "message": "Erro de validação",
  "errors": {
    "label": [
      "Este tipo de produto já existe"
    ]
  }
}
PUT /api/product-types/{id} Admin
Atualiza um tipo de produto
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: application/json
📤 Request Body
{
  "label": "Gravata Tradicional"
}
📥 Response 200 - Success
{
  "id": 1,
  "label": "Gravata Tradicional",
  "created_at": "2026-01-30T10:00:00.000000Z",
  "updated_at": "2026-01-30T15:35:00.000000Z"
}
📥 Response 404 - Not Found
{
  "message": "Tipo de produto não encontrado"
}
DELETE /api/product-types/{id} Admin
Remove um tipo de produto
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
{
  "message": "Tipo de produto removido com sucesso"
}
📥 Response 404 - Not Found
{
  "message": "Tipo de produto não encontrado"
}

📸 Links do Instagram

Rotas Públicas

GET /api/instagram-links Público
Lista todos os links do Instagram
📄 Ver Response
📥 Response 200 - Success
{
  "success": true,
  "message": "Links do Instagram recuperados com sucesso",
  "data": [
    {
      "id": 1,
      "label": "Instagram Oficial",
      "link": "https://instagram.com/cwgravatas",
      "created_at": "2026-01-30T10:00:00.000000Z",
      "updated_at": "2026-01-30T10:00:00.000000Z"
    },
    {
      "id": 2,
      "label": "Promoções",
      "link": "https://instagram.com/cwgravatas/promocoes",
      "created_at": "2026-01-30T11:00:00.000000Z",
      "updated_at": "2026-01-30T11:00:00.000000Z"
    }
  ]
}

Rotas de Administração

POST /api/instagram-links Admin
Cria um novo link do Instagram
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: application/json
📤 Request Body
{
  "label": "Instagram Stories",
  "link": "https://instagram.com/stories/cwgravatas"
}
📋 Regras de Validação
label: obrigatório, string, máximo 255 caracteres
link: obrigatório, URL válida, máximo 255 caracteres
📥 Response 201 - Created
{
  "success": true,
  "message": "Link do Instagram criado com sucesso",
  "data": {
    "id": 3,
    "label": "Instagram Stories",
    "link": "https://instagram.com/stories/cwgravatas",
    "created_at": "2026-01-30T16:00:00.000000Z",
    "updated_at": "2026-01-30T16:00:00.000000Z"
  }
}
📥 Response 422 - Validation Error
{
  "success": false,
  "message": "Dados inválidos",
  "data": {
    "label": [
      "O campo label é obrigatório"
    ],
    "link": [
      "O campo link deve ser uma URL válida"
    ]
  }
}
DELETE /api/instagram-links/{id} Admin
Remove um link do Instagram
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
{
  "success": true,
  "message": "Link do Instagram deletado com sucesso",
  "data": null
}
📥 Response 404 - Not Found
{
  "success": false,
  "message": "Link do Instagram não encontrado",
  "data": null
}

🎠 Carrossel do Home

Rotas Públicas

GET /api/home-carousel Público
Lista imagens ativas do carrossel (filtro por screen_size: ?screen_size=mobile)
📄 Ver Response
🔍 Query Parameters
screen_size: mobile | tablet | desktop (opcional)
📥 Response 200 - Success
[
  {
    "id": 1,
    "label": "Banner Principal Desktop",
    "screen_size": "desktop",
    "cloudinary_public_id": "home-carousel/banner1_abc123",
    "url": "https://res.cloudinary.com/.../banner1.jpg",
    "url_thumbnail": "https://res.cloudinary.com/.../thumb_banner1.jpg",
    "largura": 1920,
    "altura": 600,
    "tamanho_bytes": 456789,
    "ordem": 0,
    "ativo": true,
    "link": "https://example.com/produtos/destaque",
    "created_at": "2026-01-30T10:00:00.000000Z",
    "updated_at": "2026-01-30T10:00:00.000000Z"
  },
  {
    "id": 2,
    "label": "Promoção Mobile",
    "screen_size": "mobile",
    "url": "https://res.cloudinary.com/.../promo_mobile.jpg",
    "ordem": 1,
    "ativo": true,
    "link": null,
    ...
  }
]
GET /api/home-carousel/{id} Público
Mostra uma imagem específica
📄 Ver Response
📥 Response 200 - Success
{
  "id": 1,
  "label": "Banner Principal Desktop",
  "screen_size": "desktop",
  "cloudinary_public_id": "home-carousel/banner1_abc123",
  "url": "https://res.cloudinary.com/.../banner1.jpg",
  "url_thumbnail": "https://res.cloudinary.com/.../thumb_banner1.jpg",
  "largura": 1920,
  "altura": 600,
  "tamanho_bytes": 456789,
  "ordem": 0,
  "ativo": true,
  "link": "https://example.com/produtos/destaque",
  "created_at": "2026-01-30T10:00:00.000000Z",
  "updated_at": "2026-01-30T10:00:00.000000Z"
}
📥 Response 404 - Not Found
{
  "message": "Imagem não encontrada"
}

Rotas de Administração

GET /api/home-carousel/all Admin
Lista TODAS as imagens (incluindo inativas)
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
[
  {
    "id": 1,
    "label": "Banner Principal Desktop",
    "screen_size": "desktop",
    "url": "https://res.cloudinary.com/.../banner1.jpg",
    "ordem": 0,
    "ativo": true,
    ...
  },
  {
    "id": 2,
    "label": "Banner Antigo",
    "screen_size": "mobile",
    "url": "https://res.cloudinary.com/.../old_banner.jpg",
    "ordem": 5,
    "ativo": false,  // ⚠️ Inativo - visível apenas nesta rota
    ...
  }
]
POST /api/home-carousel Admin
Cria nova imagem com upload no Cloudinary
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: multipart/form-data
📤 Request Body (multipart/form-data)
label: "Banner Principal Desktop" (obrigatório)
screen_size: "desktop" | "mobile" | "tablet" (obrigatório)
imagem: [File] (jpg, jpeg, png, webp - max 5MB) (obrigatório)
ordem: 0 (opcional, padrão: 0)
ativo: true (opcional, padrão: true)
link: "https://example.com/destaque" (opcional)
📥 Response 201 - Created
{
  "id": 5,
  "label": "Banner Principal Desktop",
  "screen_size": "desktop",
  "cloudinary_public_id": "home-carousel/new_banner_xyz789",
  "url": "https://res.cloudinary.com/.../new_banner.jpg",
  "url_thumbnail": "https://res.cloudinary.com/.../thumb_new_banner.jpg",
  "largura": 1920,
  "altura": 600,
  "tamanho_bytes": 512345,
  "ordem": 0,
  "ativo": true,
  "link": "https://example.com/destaque",
  "created_at": "2026-01-30T16:00:00.000000Z",
  "updated_at": "2026-01-30T16:00:00.000000Z"
}
📥 Response 422 - Validation Error
{
  "message": "Erro de validação",
  "errors": {
    "screen_size": [
      "O screen_size deve ser: mobile, tablet ou desktop"
    ],
    "imagem": [
      "A imagem não pode ter mais de 5MB"
    ]
  }
}
PUT /api/home-carousel/{id} Admin
Atualiza imagem (pode substituir arquivo)
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: multipart/form-data
📤 Request Body (todos campos opcionais)
label: "Banner Atualizado"
screen_size: "mobile"
imagem: [File] // Se enviado, substitui a imagem no Cloudinary
ordem: 1
ativo: false
link: "https://example.com/nova-pagina"
📥 Response 200 - Success
{
  "id": 1,
  "label": "Banner Atualizado",
  "screen_size": "mobile",
  "cloudinary_public_id": "home-carousel/updated_xyz123",
  "url": "https://res.cloudinary.com/.../updated_banner.jpg",
  "ordem": 1,
  "ativo": false,
  "link": "https://example.com/nova-pagina",
  ...
}
DELETE /api/home-carousel/{id} Admin
Remove imagem (e do Cloudinary)
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
{
  "message": "Imagem removida com sucesso"
}
📥 Response 404 - Not Found
{
  "message": "Imagem não encontrada"
}
PUT /api/home-carousel/reorder Admin
Reordena imagens
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: application/json
📤 Request Body
{
  "images": [
    { "id": 3, "ordem": 0 },
    { "id": 1, "ordem": 1 },
    { "id": 2, "ordem": 2 }
  ]
}
📥 Response 200 - Success
{
  "message": "Imagens reordenadas com sucesso"
}
PATCH /api/home-carousel/{id}/toggle Admin
Ativa/desativa imagem
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
{
  "message": "Status atualizado com sucesso",
  "ativo": false  // Novo status
}

📤 Upload (Cloudinary)

POST /api/upload/imagens Admin
Upload de imagens para o Cloudinary (sem associar a produto)
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: multipart/form-data
📤 Request Body (multipart/form-data)
imagens[]: [File] (jpg, jpeg, png, webp - max 2MB cada)
imagens[]: [File]
imagens[]: [File]
// De 1 a 5 imagens por envio
📥 Response 201 - Created
{
  "message": "3 imagem(ns) enviada(s) com sucesso",
  "imagens": [
    {
      "cloudinary_public_id": "produtos/abc123def456",
      "url": "https://res.cloudinary.com/.../imagem1.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb_imagem1.jpg",
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 245678,
      "ordem": 0
    },
    {
      "cloudinary_public_id": "produtos/xyz789ghi012",
      "url": "https://res.cloudinary.com/.../imagem2.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb_imagem2.jpg",
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 198234,
      "ordem": 1
    },
    {
      "cloudinary_public_id": "produtos/mno345pqr678",
      "url": "https://res.cloudinary.com/.../imagem3.jpg",
      "url_thumbnail": "https://res.cloudinary.com/.../thumb_imagem3.jpg",
      "largura": 800,
      "altura": 600,
      "tamanho_bytes": 312456,
      "ordem": 2
    }
  ]
}

// ℹ️ Use esses dados no campo "imagens" ao criar um produto
📥 Response 422 - Validation Error
{
  "message": "The given data was invalid.",
  "errors": {
    "imagens": [
      "Máximo de 5 imagens por envio."
    ],
    "imagens.0": [
      "Cada imagem não pode ter mais de 2MB."
    ]
  }
}
DELETE /api/upload/imagem Admin
Remove imagem do Cloudinary (cancelar upload)
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: application/json
📤 Request Body
{
  "cloudinary_public_id": "produtos/abc123def456"
}
📥 Response 200 - Success
{
  "message": "Imagem removida com sucesso"
}
📥 Response 500 - Error
{
  "message": "Erro ao remover imagem"
}

👥 Usuários

GET /api/users Admin
Lista todos os usuários
📄 Ver Response
🔑 Headers Requeridos
Authorization: Bearer {token}
📥 Response 200 - Success
[
  {
    "id": 1,
    "name": "Admin User",
    "email": "admin@example.com",
    "role": "admin",
    "created_at": "2026-01-25T10:00:00.000000Z",
    "updated_at": "2026-01-25T10:00:00.000000Z"
  },
  {
    "id": 2,
    "name": "João Silva",
    "email": "joao@example.com",
    "role": "user",
    "created_at": "2026-01-28T14:30:00.000000Z",
    "updated_at": "2026-01-28T14:30:00.000000Z"
  }
]
GET /api/users/{id} Admin
Mostra um usuário específico
📄 Ver Response
📥 Response 200 - Success
{
  "id": 1,
  "name": "Admin User",
  "email": "admin@example.com",
  "role": "admin",
  "created_at": "2026-01-25T10:00:00.000000Z",
  "updated_at": "2026-01-25T10:00:00.000000Z"
}
📥 Response 404 - Not Found
{
  "message": "Usuário não encontrado"
}
POST /api/users Admin
Cria um novo usuário
📄 Ver Payload e Response
🔑 Headers Requeridos
Authorization: Bearer {token}
Content-Type: application/json
📤 Request Body
{
  "name": "Maria Santos",
  "email": "maria@example.com",
  "password": "Senha@123"
}

// ⚠️ Regras de senha:
// - Mínimo 8 caracteres
// - Pelo menos 1 letra maiúscula
// - Pelo menos 1 número
// - Pelo menos 1 símbolo especial (@$!%*#?&)
📥 Response 201 - Created
{
  "id": 3,
  "name": "Maria Santos",
  "email": "maria@example.com",
  "role": "user",
  "created_at": "2026-01-30T16:45:00.000000Z",
  "updated_at": "2026-01-30T16:45:00.000000Z"
}
📥 Response 422 - Validation Error
{
  "message": "The given data was invalid.",
  "errors": {
    "email": [
      "The email has already been taken."
    ],
    "password": [
      "A senha deve conter pelo menos uma letra maiúscula, um número e um símbolo especial (@$!%*#?&)."
    ]
  }
}
PUT /api/users/{id} Admin
Atualiza um usuário
📄 Ver Payload e Response
📤 Request Body (campos opcionais)
{
  "name": "Maria Santos Silva",
  "email": "maria.silva@example.com",
  "password": "NovaSenha@456"
  // Envie apenas os campos que deseja atualizar
}
📥 Response 200 - Success
{
  "id": 3,
  "name": "Maria Santos Silva",
  "email": "maria.silva@example.com",
  "role": "user",
  "created_at": "2026-01-30T16:45:00.000000Z",
  "updated_at": "2026-01-30T17:00:00.000000Z"
}
DELETE /api/users/{id} Admin
Remove um usuário
📄 Ver Response
📥 Response 200 - Success
{
  "message": "Usuário removido com sucesso"
}
📥 Response 404 - Not Found
{
  "message": "Usuário não encontrado"
}

💻 TypeScript Interfaces para Frontend

📦 Interfaces TypeScript

Use estas interfaces no seu projeto frontend para tipagem forte:

💙 TypeScript Types
// ========== AUTENTICAÇÃO ==========
interface LoginRequest {
  email: string;
  password: string;
}

interface LoginResponse {
  message: string;
  user: User;
  token: string;
}

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
  created_at?: string;
  updated_at?: string;
}

// ========== PRODUTOS ==========
interface Produto {
  id: number;
  imagem: string;
  imagens: ProdutoImagem[];
  nome: string;
  preco: number;
  preco_compra?: number; // Apenas para admins
  descricao: string | null;
  tamanho: string | null;
  tecido: string | null;
  cor: string | null;
  estampa: string | null;
  textura: string | null;
  largura: string | null;
  comprimento: string | null;
  observacao: string | null;
  status: boolean;
  quantidade: number;
}

interface ProdutoImagem {
  id: number;
  url: string;
  url_thumbnail: string;
  ordem: number;
  largura: number;
  altura: number;
  tamanho_bytes: number;
}

interface CreateProdutoRequest {
  nome: string;
  preco: number;
  preco_compra?: number;
  descricao?: string;
  tamanho?: string;
  tecido?: string;
  cor?: string;
  estampa?: string;
  textura?: string;
  largura?: string;
  comprimento?: string;
  observacao?: string;
  status?: boolean;
  quantidade: number;
  imagens?: ImagemUpload[];
}

interface ImagemUpload {
  cloudinary_public_id: string;
  url: string;
  url_thumbnail?: string;
  largura?: number;
  altura?: number;
  tamanho_bytes?: number;
}

// ========== TIPOS DE PRODUTOS ==========
interface ProductType {
  id: number;
  label: string;
  created_at: string;
  updated_at: string;
}

// ========== CARROSSEL ==========
interface HomeCarouselImage {
  id: number;
  label: string;
  screen_size: "mobile" | "tablet" | "desktop";
  cloudinary_public_id: string;
  url: string;
  url_thumbnail: string;
  largura: number;
  altura: number;
  tamanho_bytes: number;
  ordem: number;
  ativo: boolean;
  link: string | null;
  created_at: string;
  updated_at: string;
}

// ========== RESPONSES DE ERRO ==========
interface ErrorResponse {
  message: string;
  errors?: Record<string, string[]>;
}
⚛️ Exemplo de Serviço API (Axios)
import axios, { AxiosInstance } from 'axios';

class ApiService {
  private api: AxiosInstance;

  constructor() {
    this.api = axios.create({
      baseURL: 'http://localhost:8000/api',
      headers: { 'Content-Type': 'application/json' },
    });

    // Adiciona token automaticamente
    this.api.interceptors.request.use((config) => {
      const token = localStorage.getItem('token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });
  }

  // Autenticação
  async login(data: LoginRequest): Promise<LoginResponse> {
    const response = await this.api.post('/login', data);
    return response.data;
  }

  // Produtos
  async getProdutos(): Promise<Produto[]> {
    const response = await this.api.get('/produtos');
    return response.data;
  }

  async createProduto(data: CreateProdutoRequest): Promise<Produto> {
    const response = await this.api.post('/produtos', data);
    return response.data;
  }

  // Upload de Imagens
  async uploadImagens(files: File[]): Promise<ImagemUpload[]> {
    const formData = new FormData();
    files.forEach(file => formData.append('imagens[]', file));

    const response = await this.api.post('/upload/imagens', formData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    });
    return response.data.imagens;
  }
}

export default new ApiService();

📦 Estrutura de Respostas Padronizadas

Todas as respostas da API seguem um padrão consistente com campos success, message e data:

Resposta de Sucesso (200/201)
{
  "success": true,
  "message": "Operação realizada com sucesso",
  "data": {
    "id": 1,
    "nome": "Gravata Azul",
    "preco": "49.90"
  }
}
Resposta de Erro (400/500)
{
  "success": false,
  "message": "Descrição do erro"
}
Erro de Validação (422)
{
  "success": false,
  "message": "Os dados fornecidos são inválidos",
  "errors": {
    "nome": ["O campo nome é obrigatório."],
    "preco": ["O preço deve ser maior que zero."]
  }
}
Recurso Não Encontrado (404)
{
  "success": false,
  "message": "Produto não encontrado"
}
Não Autorizado (401)
{
  "success": false,
  "message": "Não autenticado"
}
Sem Permissão (403)
{
  "success": false,
  "message": "Acesso negado. Você não tem permissão para esta ação."
}
Resposta Paginada
{
  "success": true,
  "message": "Produtos listados com sucesso",
  "data": {
    "current_page": 1,
    "data": [
      { "id": 1, "nome": "Produto 1" },
      { "id": 2, "nome": "Produto 2" }
    ],
    "first_page_url": "http://api.cwgravatas.com/api/produtos?page=1",
    "from": 1,
    "last_page": 5,
    "last_page_url": "http://api.cwgravatas.com/api/produtos?page=5",
    "next_page_url": "http://api.cwgravatas.com/api/produtos?page=2",
    "path": "http://api.cwgravatas.com/api/produtos",
    "per_page": 15,
    "prev_page_url": null,
    "to": 15,
    "total": 73
  }
}

⚙️ Convenções e Boas Práticas

  • Type Hints: Todos os controllers usam type hints para parâmetros e return types
  • Form Requests: Validações centralizadas em classes dedicadas
  • Service Layer: Lógica de negócio encapsulada em Services
  • Traits Reutilizáveis: ApiResponse e FindsOrFails eliminam duplicação
  • Model Events: Sincronização automática de dados relacionados
  • Paginação: Listas retornam dados paginados (15-20 itens por página)
  • Cache Estratégico: Recursos estáticos cacheados por 1 hora
  • Mensagens Customizadas: Erros de validação em português