🌙 Crescent Framework

Framework web moderno e performático construído em Lua, Luvit e MySQL.

🚀 Comece Agora!

Baixe o Crescent Starter e comece a desenvolver em minutos:

📦 Download crescent-starter.zip

O que é Crescent?

Crescent é um framework web completo que combina a performance do Lua/LuaJIT com uma arquitetura moderna e modular inspirada em frameworks como NestJS e Laravel. Foi projetado para criar APIs REST e aplicações web de alto desempenho com código limpo e organizado.

🌟 Principais Características

  • ⚡ Performance: Built on LuaJIT + libuv (Luvit) for blazing fast execution
  • 🎯 Modular: Organize código em módulos independentes e reutilizáveis
  • 🗄️ ORM ActiveRecord: Interaja com banco de dados MySQL de forma elegante
  • 🔄 Migrations: Sistema completo de versionamento de schema
  • 🛠️ CLI Poderoso: Geração automática de código (como Artisan do Laravel)
  • 🔐 Segurança: Middleware de segurança, validações e hash de senhas PBKDF2
  • ✅ Testes: Biblioteca completa de assertions para testes automatizados
  • 📦 Pronto para Produção: Configuração NGINX e systemd incluídas

📦 Instalação Rápida

Método 1: Download Direto (Recomendado)

Baixe o template starter pronto para uso:

Método 1: Via Lit Package Manager

# Instale o framework via Lit
lit install daniel-m-tfs/crescent-framework

# Clone o starter template
git clone https://github.com/daniel-m-tfs/crescent-starter.git meu-projeto
cd meu-projeto

# Configure e inicie
cp .env.example .env
nano .env
luvit app.lua

Método 2: CLI (Criar Novo Projeto)

# Se você tem o CLI instalado globalmente
crescent new meu-projeto
cd meu-projeto
cp .env.example .env
nano .env
luvit app.lua

📚 Dependências

Requisitos do Sistema

  • Luvit 2.18+: Runtime Lua assíncrono
  • Lit 3.8+: Gerenciador de pacotes
  • MySQL 5.7+ ou MariaDB 10+: Banco de dados
  • Linux/macOS: Sistema operacional (Windows via WSL)

Instalando Luvit e Lit

# macOS via Homebrew
brew install luvit

# Linux - Download manual
curl -L https://github.com/luvit/lit/raw/master/get-lit.sh | sh

Dependências do Framework

O Crescent Framework automaticamente inclui:

  • luvit/luvit@2.18.1 - Runtime base
  • creationix/mysql - Driver MySQL (instalar separadamente)

Para instalar o driver MySQL:

lit install creationix/mysql

📁 Estrutura do Projeto

crescent-starter/
├── app.lua                    # 🚀 Arquivo principal da aplicação
├── bootstrap.lua              # 🔧 Bootstrap do framework
├── crescent-cli.lua          # 🛠️ CLI para geração de código
├── .env                      # 🔐 Variáveis de ambiente (não versionar!)
├── .env.example              # 📝 Template de variáveis
├── config/
│   ├── development.lua       # ⚙️ Config de desenvolvimento
│   ├── production.lua        # ⚙️ Config de produção
│   ├── nginx.conf            # 🌐 Configuração NGINX
│   └── crescent.service      # 🔄 Systemd service
├── crescent/                 # 📦 Core do framework
│   ├── init.lua
│   ├── server.lua
│   ├── core/
│   │   ├── context.lua       # Contexto HTTP (req/res)
│   │   ├── request.lua       # Request object
│   │   ├── response.lua      # Response object
│   │   └── router.lua        # Sistema de rotas
│   ├── database/
│   │   ├── model.lua         # ORM ActiveRecord
│   │   ├── query_builder.lua # Query Builder
│   │   ├── mysql.lua         # Driver MySQL
│   │   └── migrate.lua       # Sistema de migrations
│   ├── middleware/
│   │   ├── auth.lua          # Autenticação
│   │   ├── cors.lua          # CORS
│   │   ├── logger.lua        # Logging
│   │   └── security.lua      # Segurança
│   └── utils/
│       ├── env.lua           # Variáveis de ambiente
│       ├── hash.lua          # 🔐 Hash de senhas PBKDF2
│       ├── tests.lua         # ✅ Biblioteca de testes
│       ├── headers.lua       # HTTP headers
│       ├── path.lua          # Path utilities
│       └── string.lua        # String utilities
├── src/                      # 📝 Seu código (módulos)
│   └── users/                # Exemplo de módulo
│       ├── init.lua          # Registrador do módulo
│       ├── controllers/
│       │   └── users.lua
│       ├── services/
│       │   └── users.lua
│       ├── models/
│       │   └── users.lua
│       └── routes/
│           └── users.lua
├── migrations/               # 🔄 Database migrations
│   └── 20260108230701_create_users_table.lua
└── tests/                    # ✅ Testes automatizados
    └── test-*.lua

🎯 Convenções de Diretórios

  • src/: Todos os seus módulos de negócio
  • crescent/: Core do framework (não modificar)
  • config/: Arquivos de configuração
  • migrations/: Versionamento do banco de dados
  • tests/: Testes automatizados

🔧 Configuração Inicial

1. Variáveis de Ambiente (.env)

# Ambiente
ENV=development

# Banco de Dados
DB_HOST=localhost
DB_PORT=3306
DB_NAME=meu_banco
DB_USER=root
DB_PASSWORD=senha_segura

# Servidor
PORT=8080
HOST=0.0.0.0

2. Testar Conexão MySQL

-- teste-conexao.lua
local MySQL = require('crescent.database.mysql')

MySQL:test()
luvit teste-conexao.lua

3. Criar Primeira Migration

luvit crescent-cli make:migration create_products_table

4. Executar Migrations

luvit crescent-cli migrate

🎮 Primeiro Módulo

Crie um módulo CRUD completo com um único comando:

luvit crescent-cli make:module Product

Isso cria:

  • ✅ Controller (src/product/controllers/product.lua)
  • ✅ Service (src/product/services/product.lua)
  • ✅ Model (src/product/models/product.lua)
  • ✅ Routes (src/product/routes/product.lua)
  • ✅ Module Init (src/product/init.lua)

Registrar Módulo no app.lua

-- app.lua
local Crescent = require('crescent')
local app = Crescent.create()

-- Registra módulo Product
local ProductModule = require("src.product")
ProductModule.register(app)

app:listen(8080, function()
    print("✓ Servidor rodando em http://localhost:8080")
end)

🚀 Iniciando o Servidor

Modo Desenvolvimento

luvit app.lua

Ou use o CLI:

luvit crescent-cli server

Acessar API

# Listar produtos
curl http://localhost:8080/product

# Criar produto
curl -X POST http://localhost:8080/product \
  -H "Content-Type: application/json" \
  -d '{"name":"Notebook","price":2500}'

# Buscar por ID
curl http://localhost:8080/product/1

# Atualizar
curl -X PUT http://localhost:8080/product/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Notebook Dell","price":2800}'

# Deletar
curl -X DELETE http://localhost:8080/product/1

🧪 Executando Testes

# Rodar todos os testes
luvit crescent-cli test

# Rodar teste específico
luvit tests/test-users.lua

📖 Próximos Passos

Agora que você tem um projeto rodando, explore:

  1. CLI - Aprenda todos os comandos disponíveis
  2. Core Concepts - Rotas, Controllers, Services
  3. Database & ORM - Modelos, relações, migrations
  4. Utilities - Testes, hash, helpers
  5. Deployment - Deploy em produção

💡 Dicas Úteis

Hot Reload (Desenvolvimento)

Use nodemon ou entr para reload automático:

# Com entr
find . -name "*.lua" | entr -r luvit app.lua

Debug

-- Use p() para debug (pretty-print)
p(user)  -- Imprime tabela formatada
p(ctx.body)

Performance

-- Use LuaJIT JIT compilation
-- Já habilitado por padrão no Luvit

🆘 Troubleshooting

Erro: "Module not found"

# Instale dependências
lit install

Erro: "MySQL connection failed"

  1. Verifique se MySQL está rodando: mysql.server status
  2. Teste credenciais: mysql -u root -p
  3. Confira .env: DB_HOST, DB_USER, DB_PASSWORD

Porta já em uso

# Mate processo na porta 8080
lsof -ti:8080 | xargs kill -9

# Ou mude a porta no .env
PORT=3000

📚 Recursos Adicionais

🛣️ Rotas

Rotas definem os endpoints da sua API e conectam URLs aos controllers.

Definindo Rotas Básicas

-- app.lua
local Crescent = require('crescent')
local app = Crescent.create()

-- GET
app:get('/hello', function(ctx)
    return ctx.json(200, { message = "Hello World" })
end)

-- POST
app:post('/users', function(ctx)
    local body = ctx.body
    return ctx.json(201, body)
end)

-- PUT
app:put('/users/{id}', function(ctx)
    local id = ctx.params.id
    return ctx.json(200, { id = id })
end)

-- DELETE
app:delete('/users/{id}', function(ctx)
    local id = ctx.params.id
    return ctx.no_content()
end)

app:listen(8080)

Parâmetros de Rota

-- Parâmetro único
app:get('/users/{id}', function(ctx)
    local id = ctx.params.id
    return ctx.json(200, { userId = id })
end)

-- Múltiplos parâmetros
app:get('/posts/{postId}/comments/{commentId}', function(ctx)
    local postId = ctx.params.postId
    local commentId = ctx.params.commentId
    return ctx.json(200, { postId = postId, commentId = commentId })
end)

Query Parameters

app:get('/search', function(ctx)
    local query = ctx.query.q
    local page = ctx.query.page or 1
    local limit = ctx.query.limit or 10
    
    return ctx.json(200, {
        query = query,
        page = tonumber(page),
        limit = tonumber(limit)
    })
end)

-- GET /search?q=lua&page=2&limit=20

Request Body

app:post('/users', function(ctx)
    local body = ctx.body
    
    -- Acessar campos
    local name = body.name
    local email = body.email
    
    return ctx.json(201, {
        name = name,
        email = email
    })
end)

Organização em Arquivos

-- src/users/routes/users.lua
local controller = require("src.users.controllers.users")

return function(app, prefix)
    prefix = prefix or "/users"
    
    app:get(prefix, function(ctx)
        return controller:index(ctx)
    end)
    
    app:get(prefix .. "/{id}", function(ctx)
        return controller:show(ctx)
    end)
    
    app:post(prefix, function(ctx)
        return controller:create(ctx)
    end)
    
    app:put(prefix .. "/{id}", function(ctx)
        return controller:update(ctx)
    end)
    
    app:delete(prefix .. "/{id}", function(ctx)
        return controller:delete(ctx)
    end)
end

Registrar Rotas no App

-- app.lua
local userRoutes = require("src.users.routes.users")
userRoutes(app, "/api/users")

🎮 Controllers

Controllers recebem requisições HTTP e retornam respostas. Devem ser finos e delegar lógica para services.

Estrutura Básica

-- src/users/controllers/users.lua
local service = require("src.users.services.users")
local UsersController = {}

function UsersController:index(ctx)
    local users = service:getAll()
    return ctx.json(200, users)
end

function UsersController:show(ctx)
    local id = ctx.params.id
    local user = service:getById(id)
    
    if user then
        return ctx.json(200, user)
    end
    
    return ctx.json(404, { error = "User not found" })
end

function UsersController:create(ctx)
    local body = ctx.body or {}
    
    -- Validação básica
    if not body.name or not body.email then
        return ctx.json(400, { error = "Name and email are required" })
    end
    
    local user = service:create(body)
    return ctx.json(201, user)
end

function UsersController:update(ctx)
    local id = ctx.params.id
    local body = ctx.body or {}
    
    local user = service:update(id, body)
    
    if user then
        return ctx.json(200, user)
    end
    
    return ctx.json(404, { error = "User not found" })
end

function UsersController:delete(ctx)
    local id = ctx.params.id
    local success = service:delete(id)
    
    if success then
        return ctx.no_content()
    end
    
    return ctx.json(404, { error = "User not found" })
end

return UsersController

Context Object (ctx)

O objeto ctx contém toda informação da requisição:

function Controller:example(ctx)
    -- Parâmetros de rota
    local id = ctx.params.id
    
    -- Query parameters
    local page = ctx.query.page
    
    -- Request body
    local body = ctx.body
    
    -- Headers
    local auth = ctx.headers['authorization']
    
    -- Method
    local method = ctx.method  -- GET, POST, etc
    
    -- Path
    local path = ctx.path  -- /users/123
    
    -- Response helpers
    return ctx.json(200, data)
    return ctx.text(200, "Hello")
    return ctx.html(200, "<h1>Hello</h1>")
    return ctx.no_content()  -- 204
    return ctx.redirect(302, "/new-url")
end

Validação no Controller

function UsersController:create(ctx)
    local body = ctx.body or {}
    
    -- Validação manual
    local errors = {}
    
    if not body.name or body.name == "" then
        table.insert(errors, "Name is required")
    end
    
    if not body.email or not string.match(body.email, ".+@.+%.%w+") then
        table.insert(errors, "Valid email is required")
    end
    
    if #errors > 0 then
        return ctx.json(422, { errors = errors })
    end
    
    -- Criar usuário
    local user = service:create(body)
    return ctx.json(201, user)
end

Error Handling

function UsersController:create(ctx)
    local success, result = pcall(function()
        return service:create(ctx.body)
    end)
    
    if not success then
        -- Log error
        print("Error creating user:", result)
        
        return ctx.json(500, {
            error = "Internal server error",
            message = result
        })
    end
    
    return ctx.json(201, result)
end

⚙️ Services

Services contêm a lógica de negócio da aplicação. Devem ser independentes de HTTP.

Estrutura Básica

-- src/users/services/users.lua
local UsersService = {}
local User = require("src.users.models.user")

function UsersService:getAll()
    return User:all()
end

function UsersService:getById(id)
    return User:find(id)
end

function UsersService:create(data)
    -- Validação adicional
    if not data.name or #data.name < 3 then
        error("Name must be at least 3 characters")
    end
    
    return User:create(data)
end

function UsersService:update(id, data)
    local user = User:find(id)
    
    if not user then
        return nil
    end
    
    user:update(data)
    return user
end

function UsersService:delete(id)
    local user = User:find(id)
    
    if not user then
        return false
    end
    
    user:delete()
    return true
end

return UsersService

Lógica de Negócio Complexa

-- src/orders/services/orders.lua
local OrdersService = {}
local Order = require("src.orders.models.order")
local Product = require("src.products.models.product")
local User = require("src.users.models.user")

function OrdersService:createOrder(userId, items)
    -- Validar usuário
    local user = User:find(userId)
    if not user then
        error("User not found")
    end
    
    -- Validar produtos e calcular total
    local total = 0
    local validatedItems = {}
    
    for _, item in ipairs(items) do
        local product = Product:find(item.productId)
        
        if not product then
            error("Product " .. item.productId .. " not found")
        end
        
        if product.stock < item.quantity then
            error("Insufficient stock for " .. product.name)
        end
        
        local subtotal = product.price * item.quantity
        total = total + subtotal
        
        table.insert(validatedItems, {
            product_id = product.id,
            quantity = item.quantity,
            price = product.price,
            subtotal = subtotal
        })
    end
    
    -- Criar pedido
    local order = Order:create({
        user_id = userId,
        total = total,
        status = "pending"
    })
    
    -- Adicionar itens ao pedido
    for _, item in ipairs(validatedItems) do
        order:addItem(item)
        
        -- Atualizar estoque
        local product = Product:find(item.product_id)
        product:update({
            stock = product.stock - item.quantity
        })
    end
    
    return order
end

function OrdersService:cancelOrder(orderId)
    local order = Order:find(orderId)
    
    if not order then
        error("Order not found")
    end
    
    if order.status ~= "pending" then
        error("Only pending orders can be cancelled")
    end
    
    -- Devolver produtos ao estoque
    local items = order:items()
    
    for _, item in ipairs(items) do
        local product = Product:find(item.product_id)
        product:update({
            stock = product.stock + item.quantity
        })
    end
    
    -- Atualizar status
    order:update({ status = "cancelled" })
    
    return order
end

return OrdersService

Services com Transações

function OrdersService:processPayment(orderId, paymentData)
    local db = require('crescent.database.mysql')
    
    -- Iniciar transação
    db:query("START TRANSACTION")
    
    local success, err = pcall(function()
        local order = Order:find(orderId)
        
        if not order then
            error("Order not found")
        end
        
        -- Processar pagamento (API externa)
        local paymentResult = PaymentGateway:charge(paymentData)
        
        if not paymentResult.success then
            error("Payment failed: " .. paymentResult.error)
        end
        
        -- Atualizar pedido
        order:update({
            status = "paid",
            payment_id = paymentResult.id
        })
        
        -- Enviar email de confirmação
        EmailService:sendOrderConfirmation(order)
    end)
    
    if success then
        db:query("COMMIT")
        return true
    else
        db:query("ROLLBACK")
        error(err)
    end
end

💾 Models (Básico)

Models representam dados e interagem com o banco via ORM ActiveRecord.

Definição Básica

-- src/users/models/user.lua
local Model = require("crescent.database.model")

local User = Model:extend({
    table = "users",
    primary_key = "id",
    timestamps = true,
    
    fillable = {
        "name",
        "email",
        "password"
    },
    
    hidden = {
        "password"
    },
    
    validates = {
        name = { required = true, min = 3, max = 100 },
        email = { required = true, email = true, unique = true },
        password = { required = true, min = 6 }
    }
})

return User

Operações CRUD

-- CREATE
local user = User:create({
    name = "John Doe",
    email = "john@example.com",
    password = "hashed_password"
})

-- READ
local user = User:find(1)
local users = User:all()
local user = User:where({email = "john@example.com"}):first()

-- UPDATE
user:update({name = "Jane Doe"})

-- DELETE
user:delete()

Para mais detalhes sobre Models, veja Database & ORM.


🔒 Middleware

Middleware processa requisições antes que cheguem aos controllers.

Middleware de Autenticação

-- crescent/middleware/auth.lua
local Auth = {}

function Auth.middleware(ctx, next)
    local token = ctx.headers['authorization']
    
    if not token then
        return ctx.json(401, { error = "No token provided" })
    end
    
    -- Validar token
    local user = validateToken(token)
    
    if not user then
        return ctx.json(401, { error = "Invalid token" })
    end
    
    -- Adicionar usuário ao contexto
    ctx.user = user
    
    -- Continuar para próximo middleware/controller
    return next()
end

return Auth

Aplicar Middleware

-- app.lua
local auth = require('crescent.middleware.auth')

-- Global (todas as rotas)
app:use(auth.middleware)

-- Específico para rota
app:get('/protected', auth.middleware, function(ctx)
    return ctx.json(200, { user = ctx.user })
end)

Middleware de Logging

-- crescent/middleware/logger.lua
local Logger = {}

function Logger.middleware(ctx, next)
    local start = os.clock()
    
    -- Log request
    print(string.format("[%s] %s %s", os.date(), ctx.method, ctx.path))
    
    -- Executar próximo
    local response = next()
    
    -- Log response time
    local duration = (os.clock() - start) * 1000
    print(string.format("  → %d (%dms)", response.status or 200, duration))
    
    return response
end

return Logger

Middleware CORS

-- crescent/middleware/cors.lua
local CORS = {}

function CORS.middleware(ctx, next)
    -- Adicionar headers CORS
    ctx.headers['Access-Control-Allow-Origin'] = '*'
    ctx.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
    ctx.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
    
    -- Handle preflight
    if ctx.method == 'OPTIONS' then
        return ctx.no_content()
    end
    
    return next()
end

return CORS

Ordem dos Middlewares

-- app.lua
local cors = require('crescent.middleware.cors')
local logger = require('crescent.middleware.logger')
local auth = require('crescent.middleware.auth')

-- Ordem importa!
app:use(cors.middleware)      -- 1. CORS primeiro
app:use(logger.middleware)    -- 2. Logging
app:use(auth.middleware)      -- 3. Auth por último

-- Rotas
app:get('/api/users', usersController.index)

🏗️ Módulos

Módulos agrupam funcionalidades relacionadas (controllers, services, models, routes).

Estrutura de Módulo

src/users/
├── init.lua                 # Registrador do módulo
├── controllers/
│   └── users.lua
├── services/
│   └── users.lua
├── models/
│   └── user.lua
└── routes/
    └── users.lua

Module Init

-- src/users/init.lua
local Module = {}

function Module.register(app)
    -- Registrar rotas
    local routes = require("src.users.routes.users")
    routes(app, "/api/users")
    
    print("✓ Módulo Users carregado")
end

return Module

Registrar no App

-- app.lua
local Crescent = require('crescent')
local app = Crescent.create()

-- Registrar módulos
local UsersModule = require("src.users")
local ProductsModule = require("src.products")

UsersModule.register(app)
ProductsModule.register(app)

app:listen(8080)

Gerar Módulo via CLI

luvit crescent-cli make:module Product

Cria toda a estrutura automaticamente!


📊 Fluxo de Requisição

Cliente HTTP
    ↓
[Middleware CORS]
    ↓
[Middleware Logger]
    ↓
[Middleware Auth]
    ↓
[Router] → Encontra rota
    ↓
[Controller] → Recebe requisição
    ↓
[Service] → Lógica de negócio
    ↓
[Model/ORM] → Banco de dados
    ↓
[Service] ← Retorna dados
    ↓
[Controller] ← Formata resposta
    ↓
Cliente HTTP ← JSON response

💡 Boas Práticas

Controllers

  • ✅ Mantenha finos (thin controllers)
  • ✅ Delegue lógica para services
  • ✅ Valide entrada básica
  • ✅ Trate erros com try/catch (pcall)
  • ✅ Retorne status codes apropriados

Services

  • ✅ Lógica de negócio aqui
  • ✅ Independente de HTTP
  • ✅ Reutilizável entre controllers
  • ✅ Use transações quando necessário
  • ✅ Valide regras de negócio

Rotas

  • ✅ Use padrões RESTful
  • ✅ Agrupe por prefixo (/api/v1)
  • ✅ Organize em arquivos separados
  • ✅ Use nomes descritivos

Middleware

  • ✅ Ordem importa
  • ✅ CORS primeiro
  • ✅ Auth/validação depois
  • ✅ Use next() para continuar

🧪 Testando Componentes

-- tests/test-users.lua
local tests = require('crescent.utils.tests')
local UsersService = require('src.users.services.users')

local userTests = {
    testCreate = function()
        local user = UsersService:create({
            name = "Test User",
            email = "test@example.com"
        })
        
        tests.assertNotNil(user)
        tests.assertEquals(user.name, "Test User")
    end,
    
    testValidation = function()
        tests.assertError(function()
            UsersService:create({ name = "AB" })  -- Too short
        end, "at least 3 characters")
    end
}

tests.runSuite("Users Service Tests", userTests)

📖 Próximas Seções

🚀 Configuração do Banco

.env

DB_HOST=localhost
DB_PORT=3306
DB_NAME=meu_banco
DB_USER=root
DB_PASSWORD=senha123

Testar Conexão

local MySQL = require('crescent.database.mysql')
MySQL:test()

💾 Models (ORM ActiveRecord)

Definição Completa

-- src/products/models/product.lua
local Model = require("crescent.database.model")

local Product = Model:extend({
    -- Nome da tabela
    table = "products",
    
    -- Chave primária (padrão: "id")
    primary_key = "id",
    
    -- Timestamps automáticos (created_at, updated_at)
    timestamps = true,
    
    -- Soft deletes (deleted_at)
    soft_deletes = false,
    
    -- Campos que podem ser preenchidos em massa
    fillable = {
        "name",
        "description",
        "price",
        "stock",
        "category_id"
    },
    
    -- Campos escondidos em JSON (não serializar)
    hidden = {
        "deleted_at"
    },
    
    -- Campos protegidos (nunca preenchidos em massa)
    guarded = {
        "id",
        "created_at",
        "updated_at"
    },
    
    -- Validações
    validates = {
        name = {
            required = true,
            min = 3,
            max = 255,
            unique = true
        },
        price = {
            required = true,
            numeric = true,
            min = 0
        },
        stock = {
            numeric = true,
            min = 0
        },
        category_id = {
            exists = {table = "categories", column = "id"}
        }
    },
    
    -- Relações
    relations = {
        category = {
            type = "belongsTo",
            model = "Category",
            foreign_key = "category_id"
        },
        orders = {
            type = "hasMany",
            model = "OrderItem",
            foreign_key = "product_id"
        }
    }
})

-- Métodos personalizados
function Product:isLowStock()
    return self.stock < 10
end

function Product:applyDiscount(percentage)
    self.price = self.price * (1 - percentage / 100)
    self:save()
end

return Product

📝 CRUD Operations

CREATE

local Product = require('src.products.models.product')

-- Método 1: create()
local product = Product:create({
    name = "Notebook Dell",
    price = 2500,
    stock = 10
})

-- Método 2: new() + save()
local product = Product:new({
    name = "Mouse Logitech",
    price = 50
})
product:save()

READ

-- Buscar por ID
local product = Product:find(1)

-- Todos os registros
local products = Product:all()

-- Com condições
local products = Product:where({category_id = 5}):get()

-- Primeiro resultado
local product = Product:where({name = "Notebook"}):first()

-- Ordenar
local products = Product:orderBy('price', 'DESC'):get()

-- Limitar
local products = Product:limit(10):get()

-- Paginação
local products = Product:paginate(10, 1)  -- 10 por página, página 1

UPDATE

-- Método 1: Buscar e atualizar
local product = Product:find(1)
product:update({
    price = 2300,
    stock = 15
})

-- Método 2: Modificar e salvar
local product = Product:find(1)
product.price = 2300
product:save()

-- Método 3: Update direto
Product:where({id = 1}):update({price = 2300})

DELETE

-- Soft delete (se soft_deletes = true)
local product = Product:find(1)
product:delete()

-- Hard delete (força remoção permanente)
product:forceDelete()

-- Delete direto por condição
Product:where({stock = 0}):delete()

🔍 Query Builder

Condições WHERE

-- Igualdade simples
Product:where({category_id = 5}):get()

-- Múltiplas condições (AND)
Product:where({
    category_id = 5,
    stock = {">", 10}
}):get()

-- Operadores
Product:where({price = {">", 1000}}):get()
Product:where({stock = {"<=", 5}}):get()
Product:where({name = {"LIKE", "%Dell%"}}):get()

-- WHERE IN
Product:whereIn('category_id', {1, 2, 3}):get()

-- WHERE BETWEEN
Product:whereBetween('price', 1000, 5000):get()

-- WHERE NULL
Product:whereNull('deleted_at'):get()
Product:whereNotNull('discount'):get()

OR Conditions

Product:where({category_id = 5})
       :orWhere({category_id = 10})
       :get()

Ordenação

-- ASC
Product:orderBy('name'):get()
Product:orderBy('name', 'ASC'):get()

-- DESC
Product:orderBy('price', 'DESC'):get()

-- Múltiplas ordenações
Product:orderBy('category_id')
       :orderBy('price', 'DESC')
       :get()

Limit e Offset

-- LIMIT
Product:limit(10):get()

-- OFFSET
Product:offset(20):limit(10):get()

-- Paginação
Product:paginate(20, 2)  -- 20 por página, página 2

Select

-- Selecionar campos específicos
Product:select('id', 'name', 'price'):get()

-- Com alias
Product:select('id', 'name', 'price as valor'):get()

Joins

-- INNER JOIN
Product:join('categories', 'products.category_id', 'categories.id')
       :select('products.*', 'categories.name as category_name')
       :get()

-- LEFT JOIN
Product:leftJoin('categories', 'products.category_id', 'categories.id'):get()

Agrupamento

-- GROUP BY
Product:select('category_id', 'COUNT(*) as total')
       :groupBy('category_id')
       :get()

-- HAVING
Product:select('category_id', 'AVG(price) as avg_price')
       :groupBy('category_id')
       :having('avg_price', '>', 1000)
       :get()

Agregações

-- COUNT
local total = Product:count()
local inStock = Product:where({stock = {">", 0}}):count()

-- SUM
local totalValue = Product:sum('price')

-- AVG
local avgPrice = Product:avg('price')

-- MIN / MAX
local minPrice = Product:min('price')
local maxPrice = Product:max('price')

Raw Queries

-- SELECT raw
local products = Product:raw([[
    SELECT * FROM products 
    WHERE price > ? AND stock > ?
]], {1000, 0})

-- INSERT raw
Product:raw([[
    INSERT INTO products (name, price) 
    VALUES (?, ?)
]], {"Teclado", 150})

-- Com bindings para segurança (evita SQL injection)
local search = ctx.query.search
local products = Product:raw([[
    SELECT * FROM products 
    WHERE name LIKE ?
]], {"%" .. search .. "%"})

✅ Validações

Validações Disponíveis

validates = {
    -- Obrigatório
    name = { required = true },
    
    -- Tamanho string
    name = { min = 3, max = 255 },
    
    -- Numérico
    price = { numeric = true },
    
    -- Range numérico
    stock = { min = 0, max = 9999 },
    
    -- Email
    email = { email = true },
    
    -- Único na tabela
    email = { unique = true },
    
    -- Existe em outra tabela
    category_id = {
        exists = {
            table = "categories",
            column = "id"
        }
    },
    
    -- Regex customizado
    phone = { pattern = "^%d%d%d%-%d%d%d%d$" },
    
    -- Valores permitidos
    status = { in_array = {"pending", "paid", "shipped"} }
}

Validação Manual

-- No Model
function Product:validate()
    local errors = {}
    
    if not self.name or #self.name < 3 then
        table.insert(errors, "Name must be at least 3 characters")
    end
    
    if self.price and self.price < 0 then
        table.insert(errors, "Price cannot be negative")
    end
    
    if #errors > 0 then
        error(table.concat(errors, ", "))
    end
    
    return true
end

Validação no Service

-- src/products/services/products.lua
function ProductService:create(data)
    -- Validação customizada
    if not data.price or data.price <= 0 then
        error("Invalid price")
    end
    
    if data.stock and data.stock < 0 then
        error("Stock cannot be negative")
    end
    
    -- ORM faz validações do Model automaticamente
    return Product:create(data)
end

🔗 Relações

BelongsTo (N:1)

-- Product pertence a Category
local Product = Model:extend({
    table = "products",
    
    relations = {
        category = {
            type = "belongsTo",
            model = "Category",
            foreign_key = "category_id"
        }
    }
})

-- Uso
local product = Product:find(1)
local category = product:category()  -- Busca a categoria

print(category.name)

HasMany (1:N)

-- Category tem muitos Products
local Category = Model:extend({
    table = "categories",
    
    relations = {
        products = {
            type = "hasMany",
            model = "Product",
            foreign_key = "category_id"
        }
    }
})

-- Uso
local category = Category:find(1)
local products = category:products()  -- Array de produtos

for _, product in ipairs(products) do
    print(product.name)
end

HasOne (1:1)

-- User tem um Profile
local User = Model:extend({
    table = "users",
    
    relations = {
        profile = {
            type = "hasOne",
            model = "Profile",
            foreign_key = "user_id"
        }
    }
})

-- Uso
local user = User:find(1)
local profile = user:profile()

print(profile.bio)

BelongsToMany (N:N)

-- Product pertence a muitos Tags (via pivot)
local Product = Model:extend({
    table = "products",
    
    relations = {
        tags = {
            type = "belongsToMany",
            model = "Tag",
            pivot_table = "product_tags",
            foreign_key = "product_id",
            related_key = "tag_id"
        }
    }
})

-- Uso
local product = Product:find(1)
local tags = product:tags()

for _, tag in ipairs(tags) do
    print(tag.name)
end

Eager Loading

-- N+1 Problem (ruim)
local products = Product:all()
for _, product in ipairs(products) do
    local category = product:category()  -- Query por produto!
end

-- Eager Loading (bom)
local products = Product:with('category'):get()
for _, product in ipairs(products) do
    print(product.category.name)  -- Já carregado!
end

-- Múltiplas relações
local products = Product:with('category', 'tags'):get()

🔄 Migrations

Criar Migration

luvit crescent-cli make:migration create_products_table

Migration de Criação

-- migrations/20260109123456_create_products_table.lua
local Migration = {}

function Migration:up()
    return [[
        CREATE TABLE IF NOT EXISTS products (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(255) NOT NULL,
            description TEXT,
            price DECIMAL(10, 2) NOT NULL,
            stock INT DEFAULT 0,
            category_id INT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            
            INDEX idx_category (category_id),
            INDEX idx_price (price),
            
            FOREIGN KEY (category_id) 
                REFERENCES categories(id) 
                ON DELETE SET NULL
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
    ]]
end

function Migration:down()
    return [[
        DROP TABLE IF EXISTS products;
    ]]
end

return Migration

Migration de Alteração

-- migrations/20260109134500_add_discount_to_products.lua
local Migration = {}

function Migration:up()
    return [[
        ALTER TABLE products
        ADD COLUMN discount DECIMAL(5, 2) DEFAULT 0,
        ADD COLUMN is_featured BOOLEAN DEFAULT FALSE;
        
        CREATE INDEX idx_featured ON products(is_featured);
    ]]
end

function Migration:down()
    return [[
        ALTER TABLE products
        DROP COLUMN discount,
        DROP COLUMN is_featured;
        
        DROP INDEX idx_featured ON products;
    ]]
end

return Migration

Executar Migrations

# Executar pendentes
luvit crescent-cli migrate

# Desfazer última
luvit crescent-cli migrate:rollback

# Status
luvit crescent-cli migrate:status

🪝 Hooks (Lifecycle Events)

Hooks Disponíveis

local Product = Model:extend({
    table = "products",
    
    hooks = {
        before_save = function(self)
            -- Antes de salvar (CREATE ou UPDATE)
            print("Saving product:", self.name)
        end,
        
        after_save = function(self)
            -- Depois de salvar
            print("Product saved:", self.id)
        end,
        
        before_create = function(self)
            -- Antes de criar
            self.slug = slugify(self.name)
        end,
        
        after_create = function(self)
            -- Depois de criar
            print("New product created!")
        end,
        
        before_update = function(self)
            -- Antes de atualizar
        end,
        
        after_update = function(self)
            -- Depois de atualizar
        end,
        
        before_delete = function(self)
            -- Antes de deletar
        end,
        
        after_delete = function(self)
            -- Depois de deletar
        end
    }
})

Exemplo Prático

local Product = Model:extend({
    table = "products",
    
    hooks = {
        before_save = function(self)
            -- Gerar slug automaticamente
            if self.name and not self.slug then
                self.slug = self.name:lower():gsub("%s+", "-")
            end
        end,
        
        before_delete = function(self)
            -- Verificar se pode deletar
            local orderCount = OrderItem:where({product_id = self.id}):count()
            
            if orderCount > 0 then
                error("Cannot delete product with existing orders")
            end
        end,
        
        after_create = function(self)
            -- Notificar sistema
            EventBus:emit('product.created', self)
        end
    }
})

💡 Boas Práticas

Índices

-- Campos frequentemente buscados
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_status ON orders(status);

-- Chaves estrangeiras
CREATE INDEX idx_category_id ON products(category_id);

-- Compostos para queries complexas
CREATE INDEX idx_category_price ON products(category_id, price);

Transações

local db = require('crescent.database.mysql')

function OrderService:processOrder(orderData)
    db:query("START TRANSACTION")
    
    local success, err = pcall(function()
        -- Criar pedido
        local order = Order:create(orderData)
        
        -- Atualizar estoque
        for _, item in ipairs(orderData.items) do
            local product = Product:find(item.product_id)
            product:update({
                stock = product.stock - item.quantity
            })
        end
        
        -- Criar pagamento
        Payment:create({order_id = order.id, ...})
    end)
    
    if success then
        db:query("COMMIT")
        return true
    else
        db:query("ROLLBACK")
        error(err)
    end
end

N+1 Problem

-- ❌ Ruim (N+1 queries)
local products = Product:all()
for _, product in ipairs(products) do
    local category = product:category()  -- +1 query por produto
end

-- ✅ Bom (2 queries)
local products = Product:with('category'):get()
for _, product in ipairs(products) do
    print(product.category.name)  -- Já carregado
end

Paginação

-- No controller
function ProductController:index(ctx)
    local page = tonumber(ctx.query.page) or 1
    local perPage = tonumber(ctx.query.per_page) or 20
    
    local result = Product:paginate(perPage, page)
    
    return ctx.json(200, {
        data = result.data,
        current_page = result.current_page,
        total = result.total,
        per_page = result.per_page,
        last_page = result.last_page
    })
end

🧪 Testando Database

-- tests/test-product.lua
local tests = require('crescent.utils.tests')
local Product = require('src.products.models.product')

local productTests = {
    testCreate = function()
        local product = Product:create({
            name = "Test Product",
            price = 100,
            stock = 10
        })
        
        tests.assertNotNil(product)
        tests.assertNotNil(product.id)
        tests.assertEquals(product.name, "Test Product")
    end,
    
    testValidation = function()
        tests.assertError(function()
            Product:create({name = ""})  -- Empty name
        end)
    end,
    
    testRelations = function()
        local product = Product:find(1)
        local category = product:category()
        
        tests.assertNotNil(category)
        tests.assertIsTable(category)
    end
}

tests.runSuite("Product Model Tests", productTests)

📖 Próximas Seções

📋 Comandos Disponíveis

luvit crescent-cli <comando> [opções]

Comandos Principais

| Comando | Descrição | |---------|-----------| | new | Cria um novo projeto Crescent | | server | Inicia o servidor de desenvolvimento | | test | Executa todos os testes do projeto | | make:controller | Gera um controller | | make:service | Gera um service | | make:model | Gera um model | | make:routes | Gera arquivo de rotas | | make:module | Gera módulo completo (CRUD) | | make:migration | Cria uma migration | | migrate | Executa migrations pendentes | | migrate:rollback | Desfaz última migration | | migrate:status | Mostra status das migrations |

🆕 Criar Novo Projeto

luvit crescent-cli new meu-projeto
O que acontece:
  1. ✅ Clona o crescent-starter do GitHub
  2. ✅ Remove histórico Git do template
  3. ✅ Inicializa novo repositório Git
  4. ✅ Configura estrutura completa
Próximos passos após criação:
cd meu-projeto
cp .env.example .env
nano .env  # Configure MySQL
luvit app.lua

🚀 Servidor de Desenvolvimento

luvit crescent-cli server
Funcionalidades:
  • ✅ Inicia aplicação com luvit app.lua
  • ✅ Logs em tempo real
  • ✅ Substituição de processo (exec) para manter saída interativa
  • ✅ Verifica se app.lua existe antes de iniciar
Dica: Para hot-reload automático, use entr:
find . -name "*.lua" | entr -r luvit crescent-cli server

✅ Executar Testes

luvit crescent-cli test
Funcionalidades:
  • 🔍 Descobre automaticamente diretórios tests/ ou test/
  • 🔍 Encontra todos arquivos test.lua ou tests.lua
  • ▶️ Executa cada teste sequencialmente
  • 📊 Mostra saída completa de cada teste
  • 📈 Apresenta resumo final com estatísticas
  • ✅/❌ Feedback visual colorido com emojis
Exemplo de saída:
🌙 Executando Testes Crescent

ℹ Encontrados 3 arquivo(s) de teste

📄 Executando: tests/test-users.lua
────────────────────────────────────
✅ testCreate passed
✅ testUpdate passed
✅ testDelete passed

=== Results: 3/3 passed, 0 failed ===

════════════════════════════════════
🌙 Resumo dos Testes
✅ Todos os testes passaram! (3/3)

🏗️ Geradores de Código

Gerar Controller

luvit crescent-cli make:controller Product
# ou especificar módulo
luvit crescent-cli make:controller Product catalog
Cria: src/product/controllers/product.lua Template gerado:
-- src/product/controllers/product.lua
local service = require("src.product.services.product")
local ProductController = {}

function ProductController:index(ctx)
    local result = service:getAll()
    return ctx.json(200, result)
end

function ProductController:show(ctx)
    local id = ctx.params.id
    local result = service:getById(id)
    
    if result then
        return ctx.json(200, result)
    end
    return ctx.json(404, { error = "Not found" })
end

function ProductController:create(ctx)
    local body = ctx.body or {}
    local result = service:create(body)
    return ctx.json(201, result)
end

function ProductController:update(ctx)
    local id = ctx.params.id
    local body = ctx.body or {}
    local result = service:update(id, body)
    
    if result then
        return ctx.json(200, result)
    else
        return ctx.json(404, { error = "Not found" })
    end
end

function ProductController:delete(ctx)
    local id = ctx.params.id
    local success = service:delete(id)
    
    if success then
        return ctx.no_content()
    else
        return ctx.json(404, { error = "Not found" })
    end
end

return ProductController

Gerar Service

luvit crescent-cli make:service Product
Cria: src/product/services/product.lua Template gerado:
-- src/product/services/product.lua
local ProductService = {}
local Product = require("src.product.models.product")

function ProductService:getAll()
    return Product:all()
end

function ProductService:getById(id)
    return Product:find(id)
end

function ProductService:create(body)
   return Product:create(body)
end

function ProductService:update(id, body)
    local product = Product:find(id)
    if product then
        product:update(body)
        return product
    end
    return nil
end

function ProductService:delete(id)
    local product = Product:find(id)
    if product then
        product:delete()
        return true
    end
    return false
end

return ProductService

Gerar Model

luvit crescent-cli make:model Product
Cria: src/product/models/product.lua Template gerado:
-- src/product/models/product.lua
local Model = require("crescent.database.model")

local Product = Model:extend({
    table = "product",
    primary_key = "id",
    timestamps = true,
    soft_deletes = false,
    
    fillable = {
        "name",
    },
    
    hidden = {
        -- "password"
    },

    guarded = {
        -- "id", "created_at", "updated_at"
    },
    
    validates = {
        name = {required = true, min = 3, max = 255},
    },
    
    relations = {
        -- posts = {type = "hasMany", model = "Post", foreign_key = "user_id"},
    }
})

return Product

Gerar Routes

luvit crescent-cli make:routes Product
Cria: src/product/routes/product.lua Template gerado:
-- src/product/routes/product.lua
-- prefix definido em product/init.lua

local controller = require("src.product.controllers.product")

return function(app, prefix)
    prefix = prefix or "/product"
    
    -- CRUD completo
    app:get(prefix, function(ctx)
        return controller:index(ctx)
    end)
    
    app:get(prefix .. "/{id}", function(ctx)
        return controller:show(ctx)
    end)
    
    app:post(prefix, function(ctx)
        return controller:create(ctx)
    end)
    
    app:put(prefix .. "/{id}", function(ctx)
        return controller:update(ctx)
    end)
    
    app:delete(prefix .. "/{id}", function(ctx)
        return controller:delete(ctx)
    end)
end

Gerar Módulo Completo

luvit crescent-cli make:module Product
Cria tudo de uma vez:
  • ✅ Controller
  • ✅ Service
  • ✅ Model
  • ✅ Routes
  • ✅ Module Init
Estrutura gerada:
src/product/
├── init.lua
├── controllers/
│   └── product.lua
├── services/
│   └── product.lua
├── models/
│   └── product.lua
└── routes/
    └── product.lua
Module Init (src/product/init.lua):
-- src/product/init.lua
local Module = {}

function Module.register(app)
    local routes = require("src.product.routes.product")
    routes(app, "/product")
    
    print("✓ Módulo Product carregado")
end

return Module
Registrar no app.lua:
local ProductModule = require("src.product")
ProductModule.register(app)

🔄 Migrations

Criar Migration

luvit crescent-cli make:migration create_products_table
Padrões de nome reconhecidos:
  • create_xxx_table → Cria tabela "xxx"
  • add_xxx_to_yyy → Adiciona coluna na tabela "yyy"
  • drop_xxx_table → Remove tabela "xxx"
  • update_xxx_table → Atualiza tabela "xxx"
Cria: migrations/20260109123456_create_products_table.lua Template gerado:
-- migrations/20260109123456_create_products_table.lua
local Migration = {}

function Migration:up()
    return [[
        CREATE TABLE IF NOT EXISTS products (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(255) NOT NULL,
            price DECIMAL(10,2),
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
    ]]
end

function Migration:down()
    return [[
        DROP TABLE IF EXISTS products;
    ]]
end

return Migration

Executar Migrations

# Executar todas pendentes
luvit crescent-cli migrate

# Desfazer última migration
luvit crescent-cli migrate:rollback

# Ver status
luvit crescent-cli migrate:status
Exemplo de saída:
🔄 Executando migrations pendentes...

✓ 20260108230701_create_users_table.lua aplicada
✓ 20260109123456_create_products_table.lua aplicada

✅ 2 migrations executadas com sucesso!

🎯 Casos de Uso Comuns

1. Criar CRUD Completo

# 1. Criar migration
luvit crescent-cli make:migration create_categories_table

# 2. Editar migration (adicionar campos)
nano migrations/20260109_*.lua

# 3. Executar migration
luvit crescent-cli migrate

# 4. Criar módulo completo
luvit crescent-cli make:module Category

# 5. Registrar no app.lua
# Adicionar: CategoryModule.register(app)

# 6. Iniciar servidor
luvit crescent-cli server

2. Adicionar Funcionalidade a Módulo Existente

# Adicionar novo controller
luvit crescent-cli make:controller Admin users

# Adicionar novo service
luvit crescent-cli make:service Auth auth

3. Workflow de Testes

# Criar teste
touch tests/test-product.lua

# Implementar teste usando crescent/utils/tests
nano tests/test-product.lua

# Rodar testes
luvit crescent-cli test

🔧 Instalação Global do CLI

Para usar crescent sem luvit crescent-cli:

# No diretório do framework
./install.sh

# Agora use diretamente
crescent make:module Product
crescent server
crescent test

💡 Dicas Avançadas

Aliases Bash

# Adicione no ~/.bashrc ou ~/.zshrc
alias cres='luvit crescent-cli'
alias cres-serve='luvit crescent-cli server'
alias cres-test='luvit crescent-cli test'

# Uso
cres make:module Product
cres-serve
cres-test

Watch Mode com Entr

# Auto-restart no servidor
find . -name "*.lua" | entr -r luvit crescent-cli server

# Auto-run tests
find tests -name "*.lua" | entr -c luvit crescent-cli test

Scripts Personalizados

Crie scripts no package.lua ou arquivos shell:

#!/bin/bash
# dev.sh - Script de desenvolvimento

echo "🔧 Instalando dependências..."
lit install

echo "🔄 Executando migrations..."
luvit crescent-cli migrate

echo "✅ Rodando testes..."
luvit crescent-cli test

echo "🚀 Iniciando servidor..."
luvit crescent-cli server

📖 Referência Rápida

# Criar projeto
crescent new app

# Geradores
crescent make:module User
crescent make:controller Product
crescent make:service Auth
crescent make:model Category
crescent make:routes Api

# Migrations
crescent make:migration create_posts_table
crescent migrate
crescent migrate:rollback
crescent migrate:status

# Desenvolvimento
crescent server
crescent test

# Help
crescent --help

🆘 Troubleshooting

Erro: "crescent-cli.lua not found"

Execute o comando no diretório raiz do projeto onde está o arquivo crescent-cli.lua.

Erro: "Permission denied"

chmod +x crescent-cli.lua

CLI não cria arquivos

Verifique permissões de escrita no diretório:

ls -la src/

Migration falha

  1. Verifique sintaxe SQL no arquivo da migration
  2. Confirme conexão com MySQL: luvit crescent/database/mysql.lua
  3. Veja logs de erro completos

📚 Próximas Seções

✅ Sistema de Testes

O Crescent inclui uma biblioteca completa de assertions para testes automatizados.

Criar Arquivo de Teste

-- tests/test-users.lua
local tests = require('crescent.utils.tests')

local userTests = {
    testCreate = function()
        -- Seu código de teste
        tests.assertEquals(1 + 1, 2)
    end,
    
    testValidation = function()
        tests.assertTrue(true)
        tests.assertFalse(false)
    end
}

tests.runSuite("User Tests", userTests)

Executar Testes

# Todos os testes
luvit crescent-cli test

# Teste específico
luvit tests/test-users.lua

📋 Assertions Disponíveis

Comparações Básicas

-- Igualdade
tests.assertEquals(actual, expected, "mensagem opcional")
tests.assertNotEquals(actual, expected)

-- Booleanos
tests.assertTrue(value)
tests.assertFalse(value)

Comparações Numéricas

tests.assertGreaterThan(5, 3)        -- 5 > 3
tests.assertLessThan(3, 5)           -- 3 < 5
tests.assertGreaterOrEqual(5, 5)     -- 5 >= 5
tests.assertLessOrEqual(3, 5)        -- 3 <= 5
tests.assertInRange(5, 1, 10)        -- 1 <= 5 <= 10

Validações de Tipo

tests.assertType(value, "string")
tests.assertNil(value)
tests.assertNotNil(value)
tests.assertIsTable(value)
tests.assertIsFunction(value)
tests.assertIsString(value)
tests.assertIsNumber(value)
tests.assertIsBoolean(value)

Comparações de Strings

tests.assertContains("hello world", "world")
tests.assertNotContains("hello", "bye")
tests.assertStartsWith("hello world", "hello")
tests.assertEndsWith("hello world", "world")
tests.assertMatches("test123", "%d+")  -- Lua pattern

Comparações de Tables

-- Comparação profunda
tests.assertTableEquals({a=1, b=2}, {a=1, b=2})

-- Contém valor
tests.assertTableContains({1, 2, 3}, 2)

-- Vazio/não vazio
tests.assertEmpty({})
tests.assertNotEmpty({1})

-- Tamanho
tests.assertLength({1, 2, 3}, 3)
tests.assertArrayLength({1, 2, 3}, 3)

Exceptions/Errors

-- Espera erro
tests.assertError(function()
    error("Ops!")
end, "Ops!")

-- Não deve dar erro
tests.assertNoError(function()
    return 1 + 1
end)

HTTP/API Testing

-- Status code
tests.assertStatusCode(response, 200)

-- Headers
tests.assertHeader(response, "Content-Type", "application/json")

-- JSON response
tests.assertJsonEquals('{"name":"John"}', {name="John"})
tests.assertJsonContains('{"name":"John","age":30}', "name", "John")

Identidade

local obj = {}
tests.assertSame(obj, obj)        -- Mesma referência
tests.assertNotSame({}, {})       -- Objetos diferentes

📝 Exemplo Completo de Teste

-- tests/test-product.lua
local tests = require('crescent.utils.tests')
local Product = require('src.products.models.product')

local productTests = {
    testCreate = function()
        local product = Product:create({
            name = "Notebook",
            price = 2500
        })
        
        tests.assertNotNil(product)
        tests.assertIsTable(product)
        tests.assertEquals(product.name, "Notebook")
        tests.assertType(product.price, "number")
        tests.assertGreaterThan(product.price, 0)
    end,
    
    testValidation = function()
        tests.assertError(function()
            Product:create({name = ""})  -- Nome vazio
        end, "validation failed")
    end,
    
    testFind = function()
        local product = Product:find(1)
        
        if product then
            tests.assertIsTable(product)
            tests.assertNotNil(product.id)
            tests.assertType(product.name, "string")
        end
    end,
    
    testUpdate = function()
        local product = Product:find(1)
        
        if product then
            local oldName = product.name
            product:update({name = "Updated Name"})
            
            tests.assertNotEquals(product.name, oldName)
            tests.assertEquals(product.name, "Updated Name")
        end
    end,
    
    testAll = function()
        local products = Product:all()
        
        tests.assertIsTable(products)
        tests.assertGreaterOrEqual(#products, 0)
    end
}

-- Executar suite
tests.runSuite("Product Tests", productTests)
Saída:
=== Test Suite: Product Tests ===
Running test: testCreate
✅ testCreate passed
Running test: testValidation
✅ testValidation passed
Running test: testFind
✅ testFind passed
Running test: testUpdate
✅ testUpdate passed
Running test: testAll
✅ testAll passed

=== Results: 5/5 passed, 0 failed ===

🔐 Hash de Senhas (PBKDF2)

Utilitário seguro para hash de senhas com salt aleatório.

Características

  • ✅ PBKDF2 com SHA-256
  • ✅ Salt aleatório único (16 bytes)
  • ✅ 10.000 iterações por padrão
  • ✅ Timing-safe comparison
  • ✅ Senhas iguais geram hashes diferentes

Criar Hash de Senha

local hash = require('crescent.utils.hash')

-- Registrar usuário
local senha = "minhaSenhaSegura123"
local senhaHash = hash.encrypt(senha)
-- Resultado: "10000$a1b2c3d4e5f6...$9c8d7e6f5a4b..."

-- Armazenar senhaHash no banco de dados
user:update({password = senhaHash})

Verificar Senha (Login)

local hash = require('crescent.utils.hash')

-- No login
local senhaDigitada = ctx.body.password
local senhaArmazenada = user.password  -- Hash do banco

if hash.verify(senhaDigitada, senhaArmazenada) then
    -- Login bem-sucedido
    return ctx.json(200, {token = gerarToken(user)})
else
    -- Senha incorreta
    return ctx.json(401, {error = "Credenciais inválidas"})
end

Exemplo Completo (Registro + Login)

-- src/auth/services/auth.lua
local hash = require('crescent.utils.hash')
local User = require('src.users.models.user')

local AuthService = {}

function AuthService:register(data)
    -- Valida dados
    if not data.email or not data.password then
        error("Email e senha são obrigatórios")
    end
    
    -- Cria hash da senha
    local passwordHash = hash.encrypt(data.password)
    
    -- Cria usuário
    local user = User:create({
        name = data.name,
        email = data.email,
        password = passwordHash  -- Armazena hash, não senha
    })
    
    return user
end

function AuthService:login(email, password)
    -- Busca usuário
    local user = User:where({email = email}):first()
    
    if not user then
        return nil, "Usuário não encontrado"
    end
    
    -- Verifica senha
    if not hash.verify(password, user.password) then
        return nil, "Senha incorreta"
    end
    
    -- Remove senha do retorno
    user.password = nil
    
    return user
end

return AuthService

Configurar Iterações

-- Mais seguro (mais lento)
local hash50k = hash.encrypt(senha, 50000)

-- Padrão (balanceado)
local hashPadrao = hash.encrypt(senha)  -- 10.000 iterações

Aliases Disponíveis

-- Estas são equivalentes:
hash.encrypt(senha)   -- ✅ Recomendado
hash.encript(senha)   -- Alias (typo comum)

hash.verify(senha, hash)   -- ✅ Recomendado  
hash.decrypt(senha, hash)  -- Alias
hash.decript(senha, hash)  -- Alias

Hashes Simples (Checksums)

-- SHA-256 (para checksums, não senhas!)
local checksum = hash.sha256("conteúdo do arquivo")

-- MD5 (legado, não usar para senhas)
local md5sum = hash.md5("dados")

⚠️ Importante: Nunca use SHA-256 ou MD5 direto para senhas! Sempre use hash.encrypt() que implementa PBKDF2 com salt.


🌐 Variáveis de Ambiente

local env = require('crescent.utils.env')

-- Carregar .env
env.load('.env')

-- Obter valores
local dbHost = env.get('DB_HOST', 'localhost')  -- Com fallback
local port = env.get('PORT')
local isDev = env.get('ENV') == 'development'

-- Verificar se existe
if env.has('API_KEY') then
    -- usa API_KEY
end

📨 Headers HTTP

local headers = require('crescent.utils.headers')

-- Parse header string
local contentType = headers.parse('Content-Type: application/json')

-- Normalize header name
local normalized = headers.normalize('content-type')  -- "Content-Type"

-- Get header value
local value = headers.get(ctx.headers, 'authorization')

🛤️ Path Utilities

local pathUtil = require('crescent.utils.path')

-- Join paths
local fullPath = pathUtil.join('src', 'users', 'models', 'user.lua')
-- "src/users/models/user.lua"

-- Normalize path
local normalized = pathUtil.normalize('src//users/../models/./user.lua')
-- "src/models/user.lua"

-- Get directory
local dir = pathUtil.dirname('src/users/models/user.lua')
-- "src/users/models"

-- Get filename
local file = pathUtil.basename('src/users/models/user.lua')
-- "user.lua"

-- Get extension
local ext = pathUtil.extname('user.lua')
-- ".lua"

🔤 String Utilities

local stringUtil = require('crescent.utils.string')

-- Trim whitespace
local trimmed = stringUtil.trim('  hello  ')  -- "hello"

-- Split string
local parts = stringUtil.split('a,b,c', ',')  -- {"a", "b", "c"}

-- Starts/ends with
local starts = stringUtil.startsWith('hello', 'hel')  -- true
local ends = stringUtil.endsWith('hello', 'lo')  -- true

-- Case transformations
local upper = stringUtil.upper('hello')  -- "HELLO"
local lower = stringUtil.lower('HELLO')  -- "hello"
local title = stringUtil.titleCase('hello world')  -- "Hello World"

-- Slugify
local slug = stringUtil.slugify('Hello World!')  -- "hello-world"

🧪 Testando Utilities

-- tests/test-utilities.lua
local tests = require('crescent.utils.tests')
local hash = require('crescent.utils.hash')
local stringUtil = require('crescent.utils.string')

local utilTests = {
    testHashUnique = function()
        local senha = "test123"
        local hash1 = hash.encrypt(senha)
        local hash2 = hash.encrypt(senha)
        
        -- Hashes devem ser diferentes (salt único)
        tests.assertNotEquals(hash1, hash2)
        
        -- Mas ambos devem verificar corretamente
        tests.assertTrue(hash.verify(senha, hash1))
        tests.assertTrue(hash.verify(senha, hash2))
    end,
    
    testHashVerification = function()
        local senha = "myPassword123"
        local hashed = hash.encrypt(senha)
        
        tests.assertTrue(hash.verify(senha, hashed))
        tests.assertFalse(hash.verify("wrongPassword", hashed))
    end,
    
    testStringTrim = function()
        tests.assertEquals(stringUtil.trim("  hello  "), "hello")
        tests.assertEquals(stringUtil.trim("hello"), "hello")
    end,
    
    testStringSplit = function()
        local parts = stringUtil.split("a,b,c", ",")
        tests.assertArrayLength(parts, 3)
        tests.assertEquals(parts[1], "a")
        tests.assertEquals(parts[3], "c")
    end
}

tests.runSuite("Utility Tests", utilTests)

💡 Boas Práticas

Testes

  1. Organize por módulo: tests/test-{module}.lua
  2. Nomenclatura clara: testCreate, testValidation
  3. Um assert por conceito: Testes pequenos e focados
  4. Use mensagens descritivas: Facilita debug
  5. Rode antes de commit: git pre-commit hook

Hash de Senhas

  1. Nunca armazene senhas em texto plano
  2. Use hash.encrypt() sempre: PBKDF2 + salt
  3. Não use SHA-256/MD5 para senhas
  4. Valide força da senha antes: regex, tamanho mínimo
  5. Considere 2FA para produção

Variáveis de Ambiente

  1. Nunca commite .env: Use .env.example
  2. Use fallbacks: env.get('PORT', 8080)
  3. Valide valores críticos: MySQL, API keys
  4. Diferentes arquivos por ambiente: .env.dev, .env.prod

📖 Próximas Seções

📋 Pré-requisitos

Servidor Linux

# Ubuntu/Debian
sudo apt update
sudo apt install -y curl git build-essential libssl-dev

# CentOS/RHEL
sudo yum install -y curl git gcc make openssl-devel

Instalar Luvit

curl -L https://github.com/luvit/lit/raw/master/get-lit.sh | sh

Adicionar ao PATH:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Instalar NGINX

# Ubuntu/Debian
sudo apt install -y nginx

# CentOS/RHEL
sudo yum install -y nginx

# Iniciar e habilitar
sudo systemctl start nginx
sudo systemctl enable nginx

Instalar MySQL/MariaDB

# Ubuntu/Debian
sudo apt install -y mysql-server

# CentOS/RHEL
sudo yum install -y mariadb-server

# Iniciar
sudo systemctl start mysql
sudo systemctl enable mysql

# Secure installation
sudo mysql_secure_installation

🔧 Configuração do Projeto

1. Clonar Projeto

cd /var/www
sudo git clone https://github.com/seu-usuario/seu-projeto.git meu-app
sudo chown -R $USER:$USER /var/www/meu-app
cd /var/www/meu-app

2. Instalar Dependências

# Crescent Framework
lit install daniel-m-tfs/crescent-framework

# Outras dependências (se houver)
lit install luvit/secure-socket
lit install luvit/json

3. Configurar Ambiente

cp .env.example .env
nano .env
# .env (produção)
APP_ENV=production
APP_PORT=8080
APP_HOST=127.0.0.1

DB_HOST=localhost
DB_PORT=3306
DB_NAME=meu_banco_producao
DB_USER=meu_usuario
DB_PASSWORD=senha_forte_aqui

JWT_SECRET=chave_super_secreta_aleatoria_64_caracteres_ou_mais

4. Criar Banco de Dados

mysql -u root -p
CREATE DATABASE meu_banco_producao CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'meu_usuario'@'localhost' IDENTIFIED BY 'senha_forte_aqui';
GRANT ALL PRIVILEGES ON meu_banco_producao.* TO 'meu_usuario'@'localhost';
FLUSH PRIVILEGES;
EXIT;

5. Executar Migrations

luvit crescent-cli migrate

🌐 NGINX Reverse Proxy

Configuração Básica

# /etc/nginx/sites-available/meu-app
server {
    listen 80;
    server_name meuapp.com www.meuapp.com;

    # Logs
    access_log /var/log/nginx/meu-app-access.log;
    error_log /var/log/nginx/meu-app-error.log;

    # Proxy para Luvit
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        
        # Headers importantes
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        # Cache bypass
        proxy_cache_bypass $http_upgrade;
    }

    # Arquivos estáticos (se houver)
    location /static {
        alias /var/www/meu-app/public;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

Habilitar Site

# Criar link simbólico
sudo ln -s /etc/nginx/sites-available/meu-app /etc/nginx/sites-enabled/

# Testar configuração
sudo nginx -t

# Recarregar NGINX
sudo systemctl reload nginx

🔒 SSL/HTTPS com Let's Encrypt

Instalar Certbot

# Ubuntu/Debian
sudo apt install -y certbot python3-certbot-nginx

# CentOS/RHEL
sudo yum install -y certbot python3-certbot-nginx

Obter Certificado

sudo certbot --nginx -d meuapp.com -d www.meuapp.com

Configuração NGINX com SSL

# /etc/nginx/sites-available/meu-app
server {
    listen 80;
    server_name meuapp.com www.meuapp.com;
    
    # Redirecionar para HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name meuapp.com www.meuapp.com;

    # SSL
    ssl_certificate /etc/letsencrypt/live/meuapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/meuapp.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # HSTS (opcional mas recomendado)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Logs
    access_log /var/log/nginx/meu-app-access.log;
    error_log /var/log/nginx/meu-app-error.log;

    # Proxy para Luvit
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        proxy_cache_bypass $http_upgrade;
    }

    # Arquivos estáticos
    location /static {
        alias /var/www/meu-app/public;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

Renovação Automática

# Testar renovação
sudo certbot renew --dry-run

# Crontab para renovação automática
sudo crontab -e

Adicionar:

0 0 * * * certbot renew --quiet --post-hook "systemctl reload nginx"

🔧 Systemd Service

Criar Service Unit

sudo nano /etc/systemd/system/meu-app.service
[Unit]
Description=Crescent Framework Application
After=network.target mysql.service
Wants=mysql.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/meu-app
Environment="PATH=/home/www-data/.local/bin:/usr/local/bin:/usr/bin:/bin"
Environment="APP_ENV=production"
ExecStart=/home/www-data/.local/bin/luvit bootstrap.lua
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=meu-app

# Segurança
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/meu-app

[Install]
WantedBy=multi-user.target

Gerenciar Service

# Recarregar systemd
sudo systemctl daemon-reload

# Habilitar (iniciar no boot)
sudo systemctl enable meu-app

# Iniciar
sudo systemctl start meu-app

# Status
sudo systemctl status meu-app

# Parar
sudo systemctl stop meu-app

# Reiniciar
sudo systemctl restart meu-app

# Logs em tempo real
sudo journalctl -u meu-app -f

📊 Monitoramento e Logs

Logs Systemd

# Últimas 100 linhas
sudo journalctl -u meu-app -n 100

# Seguir em tempo real
sudo journalctl -u meu-app -f

# Filtrar por data
sudo journalctl -u meu-app --since "2026-01-09"

# Filtrar por prioridade
sudo journalctl -u meu-app -p err

Logs NGINX

# Access log
sudo tail -f /var/log/nginx/meu-app-access.log

# Error log
sudo tail -f /var/log/nginx/meu-app-error.log

# Analisar códigos de status
awk '{print $9}' /var/log/nginx/meu-app-access.log | sort | uniq -c | sort -rn

# Top 10 IPs
awk '{print $1}' /var/log/nginx/meu-app-access.log | sort | uniq -c | sort -rn | head -10

Logs da Aplicação

-- Implementar logger
local Logger = {}

function Logger:log(level, message, data)
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    local logEntry = string.format(
        "[%s] [%s] %s",
        timestamp,
        level,
        message
    )
    
    if data then
        logEntry = logEntry .. " " .. require('json').encode(data)
    end
    
    print(logEntry)  -- systemd captura isso
end

function Logger:info(message, data)
    self:log("INFO", message, data)
end

function Logger:error(message, data)
    self:log("ERROR", message, data)
end

return Logger

⚡ Performance

NGINX Optimizations

# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

http {
    # Básico
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    
    # Buffer
    client_body_buffer_size 128k;
    client_max_body_size 10M;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;
    
    # Gzip
    gzip on;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript 
               application/json application/javascript application/xml+rss;
    gzip_disable "msie6";
    
    # Cache estático
    open_file_cache max=10000 inactive=30s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=auth:10m rate=3r/s;
}

MySQL Optimization

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# InnoDB
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT

# Query cache
query_cache_size = 64M
query_cache_type = 1

# Connections
max_connections = 200

# Logs (desabilitar em produção para performance)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2

Aplicar:

sudo systemctl restart mysql

🔐 Segurança

Firewall (UFW)

# Habilitar UFW
sudo ufw enable

# Permitir SSH
sudo ufw allow 22/tcp

# Permitir HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Verificar status
sudo ufw status

Fail2Ban

# Instalar
sudo apt install -y fail2ban

# Configurar
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5

[sshd]
enabled = true

[nginx-http-auth]
enabled = true

[nginx-limit-req]
enabled = true
# Iniciar
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Status
sudo fail2ban-client status

Hardening MySQL

-- Remover usuários anônimos
DELETE FROM mysql.user WHERE User='';

-- Remover banco de teste
DROP DATABASE IF EXISTS test;

-- Permitir root apenas localmente
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');

FLUSH PRIVILEGES;

Variáveis de Ambiente Seguras

# Nunca commite .env no Git!
# Usar variáveis do sistema

sudo nano /etc/systemd/system/meu-app.service
[Service]
Environment="DB_PASSWORD=senha_segura_aqui"
Environment="JWT_SECRET=token_secreto_aqui"

🔄 Deploy Automatizado

Script de Deploy

#!/bin/bash
# deploy.sh

set -e  # Exit on error

APP_DIR="/var/www/meu-app"
BACKUP_DIR="/var/backups/meu-app"

echo "🚀 Starting deployment..."

# 1. Backup atual
echo "📦 Creating backup..."
mkdir -p $BACKUP_DIR
tar -czf $BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz $APP_DIR

# 2. Pull código
echo "📥 Pulling latest code..."
cd $APP_DIR
git pull origin main

# 3. Instalar dependências
echo "📚 Installing dependencies..."
lit install

# 4. Migrations
echo "🗄️ Running migrations..."
luvit crescent-cli migrate

# 5. Reiniciar serviço
echo "🔄 Restarting service..."
sudo systemctl restart meu-app

# 6. Verificar status
echo "✅ Checking status..."
sleep 2
sudo systemctl status meu-app --no-pager

# 7. Reload NGINX
echo "🌐 Reloading NGINX..."
sudo nginx -t && sudo systemctl reload nginx

echo "✨ Deployment complete!"

Tornar executável:

chmod +x deploy.sh

GitHub Actions

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Deploy via SSH
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SERVER_IP }}
        username: ${{ secrets.SERVER_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/meu-app
          ./deploy.sh

🩺 Health Checks

Endpoint de Status

-- src/health/routes/health.lua
return function(router)
    router:get("/health", function(ctx)
        -- Verificar banco
        local db = require('crescent.database.mysql')
        local dbOk = pcall(function()
            db:query("SELECT 1")
        end)
        
        return ctx.json(200, {
            status = "ok",
            timestamp = os.date("%Y-%m-%d %H:%M:%S"),
            uptime = os.clock(),
            database = dbOk and "connected" or "disconnected"
        })
    end)
end

Monitoramento Externo

# Criar script de verificação
nano /usr/local/bin/check-app.sh
#!/bin/bash

URL="https://meuapp.com/health"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $URL)

if [ $RESPONSE -eq 200 ]; then
    echo "✅ App is healthy"
    exit 0
else
    echo "❌ App is down (HTTP $RESPONSE)"
    # Reiniciar serviço
    sudo systemctl restart meu-app
    exit 1
fi
chmod +x /usr/local/bin/check-app.sh

# Cron a cada 5 minutos
crontab -e
*/5 * * * * /usr/local/bin/check-app.sh >> /var/log/app-health.log 2>&1

🐛 Troubleshooting

App não inicia

# Verificar logs
sudo journalctl -u meu-app -n 100

# Verificar sintaxe Lua
luvit -e "require('bootstrap')"

# Verificar permissões
ls -la /var/www/meu-app
sudo chown -R www-data:www-data /var/www/meu-app

Erro 502 Bad Gateway

# App está rodando?
sudo systemctl status meu-app

# Porta correta?
netstat -tulpn | grep 8080

# Testar localmente
curl http://localhost:8080

Banco de dados não conecta

# MySQL está rodando?
sudo systemctl status mysql

# Testar conexão
mysql -h localhost -u meu_usuario -p meu_banco_producao

# Verificar .env
cat .env | grep DB_

Logs grandes

# Limitar tamanho do journal
sudo journalctl --vacuum-time=7d
sudo journalctl --vacuum-size=500M

# Rotação de logs NGINX
sudo nano /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        systemctl reload nginx > /dev/null
    endscript
}

📖 Próximas Seções