🌙 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: 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)

# Rodando de dentro do diretório do Crescent Framework
# (a instalação global do CLI ainda não funciona de qualquer diretório —
# ver Troubleshooting)
luvit crescent-cli.lua 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
  • openssl (via lua-openssl, já embutido no binário do Luvit — confirme

com luvit -v) - usado por crescent.utils.hash (PBKDF2) e, quando disponível, por crescent.utils.jwt para HMAC-SHA256

O driver MySQL não é um pacote Lit — é um módulo LuaRocks, instale separadamente:

luarocks install luasql-mysql

Sem ele, o framework roda em modo mock (avisa no console e não conecta a um MySQL real), então dá pra seguir o resto do tutorial sem instalar nada ainda.


📁 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
APP_ENV=development

# Servidor
APP_HOST=0.0.0.0
APP_PORT=8080

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

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.new()

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

-- app:listen(port, host) não recebe callback; "listening" já é
-- impresso pelo próprio framework
app:listen(8080)

🚀 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 um arquivo de teste específico (substitua pelo nome real do seu teste)
luvit tests/test-product.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
APP_PORT=3000

CLI global (crescent) não encontrado ou dá erro de módulo

A instalação global do CLI (comando crescent, via install.sh) hoje só funciona corretamente quando rodada de dentro do diretório do Crescent Framework — crescent-cli.lua resolve require("crescent.utils....") relativo ao diretório atual, e não ajusta package.path como o bootstrap.lua de um projeto faz. Rodando de outro diretório, use o caminho completo: luvit /caminho/para/Crescent\ Framework/crescent-cli.lua <comando>.


📚 Recursos Adicionais


🛣️ Rotas

Entenda os principais componentes da arquitetura Crescent.

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.new()

-- 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("/new-url", 302)  -- (location, status) — nessa ordem
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 OrderItem = require("src.orders.models.order_item")
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 (order_items é uma tabela separada — o Model
    -- não tem um `addItem`/`items` prontos, então isso é feito na mão aqui;
    -- se quiser algo reutilizável, defina um método customizado na classe
    -- Order, como `function Order:isLowStock()` é mostrado em database.md)
    for _, item in ipairs(validatedItems) do
        OrderItem:create({
            order_id = order.id,
            product_id = item.product_id,
            quantity = item.quantity,
            price = item.price
        })

        -- 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 = OrderItem:where("order_id", order.id):get()

    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

⚠️ Isso não é uma transação atômica de verdade hoje. crescent/database/mysql.lua pega uma conexão do pool a cada chamada de query() e devolve ao final dela — não há garantia de que START TRANSACTION, as queries do meio e o COMMIT/ROLLBACK rodem na mesma conexão. Trate como limitação conhecida (ver Database & ORM) até o driver expor uma conexão dedicada por transação.


💾 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_length = 3, max_length = 100 },
        email = { required = true, email = true, unique = true },
        password = { required = true, min_length = 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. crescent/middleware/{auth,logger,cors,security,static}.lua já trazem implementações prontas — normalmente você só precisa registrá-las com app:use(...), sem reescrever nada.

⚠️ Middleware no Crescent é sempre global. app:use(middleware) registra a função pra rodar em toda requisição — não existe app:get(path, middleware, handler) nem middleware escopado por grupo de rotas (Server:group(prefix, fn) só agrupa prefixo de path, não aplica middleware). Se precisar proteger só algumas rotas, ou você escreve um middleware que confere ctx.path e deixa passar (return next()) pras rotas públicas, ou faz a checagem manualmente dentro do handler daquela rota específica.

Middleware de Autenticação (JWT pronto)

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

-- Protege TODAS as rotas registradas depois desta linha
app:use(auth.jwt({ secret = env.get("JWT_SECRET") }))

auth.jwt(options) verifica o header Authorization: Bearer <token>, valida o JWT e guarda o payload em ctx.state.jwt_payload / ctx.state.user (não ctx.user). auth.protected(options) é um atalho pra auth.jwt(options). Ver crescent/middleware/auth.lua pra bearer, basic e api_key, e Utilities para a API completa.

Protegendo só algumas rotas

Como middleware é sempre global, pra proteger só um prefixo de rotas escreva um middleware que decide com base em ctx.path:

local jwt_middleware = auth.jwt({ secret = env.get("JWT_SECRET") })

app:use(function(ctx, next)
    if ctx.path:match("^/admin") then
        return jwt_middleware(ctx, next)
    end
    return next()
end)

Middleware de Logging

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

app:use(logger.basic())     -- uma linha por requisição
-- ou
app:use(logger.detailed())  -- log completo (headers, query, params)

Middleware CORS

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

app:use(cors.create({
    origin = "*",
    methods = "GET,POST,PUT,PATCH,DELETE,OPTIONS",
    headers = "Content-Type, Authorization",
    credentials = false
}))

-- ou o preset permissivo de desenvolvimento
app:use(cors.default())

Headers de resposta (CORS ou qualquer outro) sempre passam por ctx.res:setHeader(nome, valor)ctx.headers é só a tabela de headers da requisição (somente leitura, na prática) e escrever nela não afeta a resposta enviada.

Ordem dos Middlewares

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

-- Ordem importa! Cada app:use() roda antes das rotas, na ordem registrada
app:use(cors.default())                                     -- 1. CORS primeiro
app:use(logger.basic())                                     -- 2. Logging
app:use(auth.jwt({ secret = env.get("JWT_SECRET") }))        -- 3. Auth por último

-- Rotas
app:get('/api/users', function(ctx)
    return usersController:index(ctx)
end)

🏗️ 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.new()

-- 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

Sistema de ORM ActiveRecord, migrations e query builder.

.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 delete: em vez de DELETE, escreve deleted_at e faz UPDATE
    soft_deletes = false,

    -- Campos que podem ser preenchidos em massa
    fillable = {
        "name",
        "description",
        "price",
        "stock",
        "category_id"
    },

    -- Campos escondidos em toArray()/toJSON() (não serializar)
    hidden = {
        "deleted_at"
    },

    -- Campos protegidos (nunca preenchidos em massa; tem prioridade sobre fillable)
    guarded = {
        "id",
        "created_at",
        "updated_at"
    },

    -- Validações (regras suportadas: required, min_length, max_length, email, unique)
    validates = {
        name = { required = true, min_length = 3, max_length = 255 },
        price = { required = true }
    },

    -- Relações: cada chave é uma função que recebe a instância e devolve
    -- o resultado de hasMany/hasOne/belongsTo (ver seção "Relações" abaixo)
    relations = {
        category = function(self)
            local Category = require("src.categories.models.category")
            return self:belongsTo(Category, "category_id")
        end
    },

    -- Hooks ficam direto na raiz da config (não dentro de um bloco "hooks")
    before_save = function(self)
        if self.name and not self.slug then
            self.slug = self.name:lower():gsub("%s+", "-")
        end
    end
})

-- 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

validates, relations e os hooks (before_create, after_create, before_save, after_save, before_update, after_update, before_delete, after_delete) são todos opcionais.


📝 CRUD Operations

CREATE

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

-- Método 1: create() — valida, roda hooks, insere e devolve a instância
local product, errors = Product:create({
    name = "Notebook Dell",
    price = 2500,
    stock = 10
})

if not product then
    -- validação falhou (ou o insert falhou) — não lança erro, devolve nil + errors
    print(errors)
end

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

READ

-- Buscar por ID (devolve instância do Model ou nil)
local product = Product:find(1)

-- Buscar por ID ou lançar erro
local product = Product:findOrFail(1)

-- Todos os registros (array de instâncias do Model)
local products = Product:all()

-- Primeiro resultado (instância do Model)
local product = Product:first()

-- Com condições — where(coluna, operador, valor) ou where(coluna, valor) (operador = "=")
-- IMPORTANTE: Product:where(...) devolve um QueryBuilder, não instâncias do Model —
-- :get()/:first() aqui devolvem tabelas cruas do banco, sem os métodos do Model
local rows = Product:where("category_id", 5):get()
local row = Product:where("name", "Notebook"):first()

-- Ordenar, limitar (métodos do QueryBuilder — encadeiam depois de where()/query())
local products = Product:where("stock", ">", 0):orderBy("price", "DESC"):get()
local products = Product:query():limit(10):get()

-- Paginação manual (paginate só existe no QueryBuilder, não no Model)
local page, per_page = 1, 10
local products = Product:query():paginate(page, per_page):get()

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 via QueryBuilder (não roda hooks nem timestamps do Model)
Product:where("id", 1):update({price = 2300})

DELETE

-- Se soft_deletes = true, isto grava deleted_at e faz UPDATE em vez de DELETE
local product = Product:find(1)
product:delete()

-- Delete direto por condição via QueryBuilder (ignora soft_deletes e hooks)
Product:where("stock", 0):delete()

🔍 Query Builder

Os métodos abaixo existem no QueryBuilder (crescent/database/query_builder.lua). No Model, só query(), find(), findOrFail(), first(), all(), where() e raw() existem como atalhos estáticos — para qualquer outro método (join, orderBy, limit, whereIn, paginate, count, ...) comece a cadeia com Product:query() ou Product:where(...).

Condições WHERE

-- Igualdade simples
Product:where("category_id", 5):get()

-- Operador explícito
Product:where("price", ">", 1000):get()
Product:where("stock", "<=", 5):get()

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

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

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

Não existe whereBetween. Para isso, use duas condições (:where("price", ">=", 1000):where("price", "<=", 5000)) ou uma raw() query.

OR Conditions

Product:where("category_id", 5)
       :orWhere("category_id", 10)
       :get()

Ordenação

-- ASC (padrão)
Product:query():orderBy("name"):get()

-- DESC
Product:query():orderBy("price", "DESC"):get()

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

A direção é sempre normalizada para ASC ou DESC — qualquer outro valor vira ASC.

Limit e Offset

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

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

-- Paginação (limit/offset prontos; ainda precisa de :get())
Product:query():paginate(2, 20):get() -- página 2, 20 por página

paginate(page, per_page) só ajusta limit/offset — não devolve total de registros nem número de páginas. Se precisar dessa metadata, calcule com uma query :count() separada (veja "Paginação" em Boas Práticas).

Select

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

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

Joins

-- INNER JOIN (operador "=" é o padrão se omitido)
Product:query()
       :join("categories", "products.category_id", "categories.id")
       :select("products.*", "categories.name as category_name")
       :get()

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

Agregações

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

count() existe hoje — não há sum/avg/min/max/groupBy/having prontos no QueryBuilder. Para esses casos, use raw().

Raw Queries

-- SELECT raw (sempre use bindings com ? — nunca concatene valor de usuário na string)
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

Só estas 5 regras existem em Model:validate() hoje:

validates = {
    -- Obrigatório
    name = { required = true },

    -- Tamanho mínimo/máximo de string
    name = { min_length = 3, max_length = 255 },

    -- Email (regex simples)
    email = { email = true },

    -- Único na tabela (ignora o próprio registro ao atualizar)
    email = { unique = true }
}

Não há numeric, range numérico, exists (checagem de FK), pattern (regex customizado) ou in_array embutidos. Para essas validações, valide manualmente no Service (próxima seção) — é o padrão recomendado hoje.

Validação no Service

-- src/products/services/products.lua
function ProductService:create(data)
    -- Validação customizada (o que o Model:validate() não cobre)
    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

    -- Product:create() já roda as validações do Model (required/min_length/etc)
    local product, errors = Product:create(data)
    if not product then
        error(table.concat((function()
            local msgs = {}
            for _, msg in pairs(errors) do table.insert(msgs, msg) end
            return msgs
        end)(), ", "))
    end

    return product
end

🔗 Relações

Diferente de outros ORMs, relações no Crescent não são declarativas — cada relação é uma função Lua que você chama explicitamente ou registra em relations para carregar sob demanda via instance:get("nome").

Três helpers de instância fazem o trabalho pesado:

Método Uso Retorno
self:belongsTo(RelatedModel, foreign_key, owner_key?) N:1 instância do RelatedModel (ou nil)
self:hasMany(RelatedModel, foreign_key, local_key?) 1:N QueryBuilder (chame :get())
self:hasOne(RelatedModel, foreign_key, local_key?) 1:1 linha crua da tabela (não é instância do Model)

Uso direto (sem configurar relations)

local Category = require("src.categories.models.category")
local Product = require("src.products.models.product")

local product = Product:find(1)
local category = product:belongsTo(Category, "category_id")
print(category.name)

local category2 = Category:find(1)
local products = category2:hasMany(Product, "category_id"):get()
for _, row in ipairs(products) do
    print(row.name)
end

Registrando em relations (carregamento preguiçoso e cacheado)

local Product = Model:extend({
    table = "products",
    relations = {
        category = function(self)
            local Category = require("src.categories.models.category")
            return self:belongsTo(Category, "category_id")
        end
    }
})

local product = Product:find(1)
local category = product:get("category") -- NÃO product:category() nem product.category

instance:get("category") chama a função uma única vez e guarda o resultado em cache na própria instância; chamadas seguintes reaproveitam o valor.

Não há belongsToMany (N:N com tabela pivot) nem eager loading (:with(...)) hoje. Relações N:N precisam ser resolvidas manualmente com raw() ou um join(). Para evitar N+1 em listagens, veja "N+1 Problem" em Boas Práticas.


🔄 Migrations

Criar Migration

luvit crescent-cli make:migration create_products_table

Isso gera um arquivo em migrations/ com uma tabela mínima (id, name, created_at, updated_at) — edite o SQL gerado para o schema real.

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

Cada migration roda com uma única chamada MySQL:query(sql) por cima do driver — evite depender de múltiplas instruções separadas por ; num só up()/down() funcionando como transação; prefira uma instrução DDL por migration quando possível.

Executar Migrations

# Executar pendentes
luvit crescent-cli migrate

# Desfazer última
luvit crescent-cli migrate:rollback

# Status
luvit crescent-cli migrate:status

🪝 Hooks (Lifecycle Events)

Os hooks ficam direto na raiz da config passada para Model:extend() — não existe um bloco hooks = {...} agrupando eles.

local Product = Model:extend({
    table = "products",

    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 = self.name:lower():gsub("%s+", "-")
    end,

    after_create = function(self)
        -- Depois de criar
        print("New product created!")
    end,

    before_update = function(self) end,
    after_update = function(self) end,

    before_delete = function(self)
        -- Antes de deletar (soft ou hard)
        local orderCount = OrderItem:where("product_id", self.id):count()
        if orderCount > 0 then
            error("Cannot delete product with existing orders")
        end
    end,

    after_delete = function(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

O mysql.lua atual pega uma conexão do pool por chamada e a devolve ao fim dela — chamadas separadas (db:query("START TRANSACTION"), depois outras queries, depois db:query("COMMIT")) não têm garantia de rodar na mesma conexão, então isso não funciona como uma transação atômica de verdade. Não há suporte a transações no ORM hoje; trate isso como uma limitação conhecida (e evite depender de rollback automático) até que o driver exponha uma conexão dedicada por transação.

N+1 Problem

-- ❌ Ruim (N+1 queries: 1 pra listar produtos + 1 por produto)
local products = Product:all()
for _, product in ipairs(products) do
    local category = product:belongsTo(Category, "category_id")
end

-- ✅ Melhor: uma query pros produtos + uma query batelada pras categorias
local products = Product:all()
local category_ids = {}
for _, product in ipairs(products) do
    table.insert(category_ids, product.category_id)
end

local categories_by_id = {}
for _, row in ipairs(Category:query():whereIn("id", category_ids):get()) do
    categories_by_id[row.id] = row
end

for _, product in ipairs(products) do
    local category = categories_by_id[product.category_id]
end

Não há eager loading (:with(...)) embutido — o padrão acima (buscar IDs e fazer um whereIn batelado) é a forma recomendada de evitar N+1 hoje.

Paginação

paginate() só ajusta limit/offset; total de registros e número de páginas precisam ser calculados à parte:

-- No controller
function ProductController:index(ctx)
    local page = tonumber(ctx.query.page) or 1
    local per_page = tonumber(ctx.query.per_page) or 20

    local total = Product:query():count()
    local data = Product:query():paginate(page, per_page):get()

    return ctx.json(200, {
        data = data,
        current_page = page,
        per_page = per_page,
        total = total,
        last_page = math.ceil(total / per_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()
        -- Product:create() não lança erro em falha de validação —
        -- devolve nil + uma tabela de erros
        local product, errors = Product:create({name = ""})
        tests.assertNil(product)
        tests.assertNotNil(errors)
    end,

    testRelations = function()
        local Category = require('src.categories.models.category')
        local product = Product:find(1)
        local category = product:belongsTo(Category, "category_id")

        tests.assertNotNil(category)
        tests.assertIsTable(category)
    end
}

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

📖 Próximas Seções


📋 Comandos Disponíveis

O Crescent CLI é uma ferramenta poderosa para acelerar o desenvolvimento, gerando código automaticamente e gerenciando seu projeto.

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

Comandos Principais

Comando Descrição
new <nome> 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 (real, capturada rodando runSuite e o comando test):

🌙 Executando Testes Crescent

ℹ Encontrados 1 arquivo(s) de teste

📄 Executando: tests/test-product.lua
────────────────────────────────────────────────────────────

=== Test Suite: Product Model Tests ===
Running test: testCreate
✅ testCreate passed
Running test: testUpdate
✅ testUpdate passed
Running test: testDelete
✅ testDelete passed

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

════════════════════════════════════════════════════════════
🌙 Resumo dos Testes

Total de arquivos executados: 1

✅ Todos os testes passaram! (1/1)

Cada suíte (tests.runSuite) sempre imprime === Test Suite: <nome> === e uma linha Running test: <nome> antes de cada resultado — é assim que crescent/utils/tests.lua funciona por baixo, independente de rodar via luvit crescent-cli test ou chamando o arquivo de teste direto.


🏗️ 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
-- Controller para Product

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
-- Service para lógica de negócio de Product

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
-- Model para Product usando Active Record ORM

local Model = require("crescent.database.model")

local Product = Model:extend({
    table = "product",
    primary_key = "id",
    timestamps = true,
    soft_deletes = false,
    
    fillable = {
        -- Adicione aqui os campos que podem ser preenchidos em massa
        "name",
    },
    
    hidden = {
        -- Campos que não devem aparecer em JSON/serialização
        -- "password"
    },

    guarded = {
        -- Campos protegidos contra mass assignment
        -- "id", "created_at", "updated_at"
    },
    
    validates = {
        -- Adicione validações aqui
        name = {required = true, min_length = 3, max_length = 255},
    },

    relations = {
        -- Defina relações aqui (cada uma é uma função que recebe a instância)
        -- posts = function(self) return self:hasMany(require("src.posts.models.post"), "user_id") end,
        -- profile = function(self) return self:belongsTo(require("src.profile.models.profile"), "user_id") end,
    }
})

-- Métodos personalizados do model
-- function Product:customMethod()
--     -- Seu código aqui
-- end

return Product

Veja Database & ORM → Relações para mais detalhes de como relations funciona na prática.

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:

O nome da migration só decide qual nome de tabela é extraído — o SQL gerado é sempre o mesmo (CREATE TABLE IF NOT EXISTS <tabela> (...) no up(), DROP TABLE IF EXISTS <tabela> no down()), não importa qual dos 4 padrões você usa:

  • create_xxx_table → tabela extraída: "xxx"
  • add_xxx_to_yyy → tabela extraída: "yyy"
  • drop_xxx_table → tabela extraída: "xxx"
  • update_xxx_table → tabela extraída: "xxx"

Se o nome não bater com nenhum desses padrões, a tabela extraída é "example". O gerador não cria colunas específicas nem gera ALTER TABLE/ADD COLUMN — sempre produz a mesma tabela genérica (id, name, created_at, updated_at); editar up()/down() à mão pra adicionar colunas/índices reais é o fluxo esperado (veja exemplos em Database & ORM → Migrations).

Cria: migrations/20260109123456_create_products_table.lua

Template gerado (sempre igual, independente do nome da migration):

-- migrations/20260109123456_create_products_table.lua
-- Migration: create_products_table

local Migration = {}

-- Executa a migration (criar tabelas, adicionar colunas, etc)
function Migration:up()
    return [[
        CREATE TABLE IF NOT EXISTS products (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(255) NOT NULL,
            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

-- Desfaz a migration (remover tabelas, colunas, etc)
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 (real, formato de crescent/database/migrate.lua):

🌙 Executando Migrations

→ Executando: 20260108230701_create_users_table
  Executada com sucesso!
→ Executando: 20260109123456_create_products_table
  Executada com sucesso!

Total: 2 migration(s) executada(s)

🎯 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

Crescent fornece diversas utilidades prontas para acelerar seu desenvolvimento.

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

# Um arquivo 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()
        -- Product:create() não lança erro em validação inválida —
        -- devolve nil + uma tabela de erros (retorno múltiplo)
        local product, errors = Product:create({name = ""})

        tests.assertNil(product)
        tests.assertNotNil(errors)
    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 (where() é posicional: coluna, [operador,] valor)
    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

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.

hash.encrypt()/hash.verify() são os únicos nomes válidos hoje — os aliases antigos (encript, decrypt, decript) foram removidos.


🌐 APIs Externas

crescent.utils.http é um cliente HTTP estilo axios para consumir APIs externas, construído sobre socket.http/ssl.https/ltn12/cjson (dependências LuaRocks: luasocket, luasec, lua-cjson).

Uso Básico (instância padrão)

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

-- GET
local result, err = http.get("https://api.exemplo.com/users/1")
if result then
    print(result.status)   -- 200
    print(result.data)     -- corpo já decodificado como tabela se for JSON
else
    print(err.message)     -- "Request failed with status 404"
end

-- POST com corpo JSON (Content-Type: application/json é setado automaticamente)
local result, err = http.post("https://api.exemplo.com/users", {
    name = "João",
    email = "joao@example.com"
})

-- PUT / PATCH / DELETE / HEAD / OPTIONS
http.put(url, data)
http.patch(url, data)
http.delete(url)
http.head(url)
http.options(url)

-- Requisição genérica
local result, err = http.request({
    url = "https://api.exemplo.com/search",
    method = "GET",
    params = { q = "crescent" },   -- vira ?q=crescent na URL
    headers = { ["x-api-key"] = "..." }
})

Retorno: em sucesso, (result, nil)result tem data (corpo, já decodificado se for JSON válido), status, statusText, headers, config, request. Em falha, (nil, result) — o mesmo formato, mais result.error = true e result.message.

Instância customizada (baseURL, headers e timeout fixos)

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

local api = http.create({
    baseURL = "https://api.exemplo.com",
    timeout = 10,
    headers = { ["Authorization"] = "Bearer " .. token }
})

local result, err = api:get("/users/1")  -- vira https://api.exemplo.com/users/1
local result, err = api:post("/users", { name = "João" })

🔐 JWT (JSON Web Tokens)

O Crescent fornece suporte a autenticação JWT no runtime Luvit (não OpenResty/nginx — são runtimes Lua diferentes). crescent.utils.jwt tenta usar openssl.hmac.digest para o HMAC-SHA256 quando disponível (o módulo openssl já vem embutido no binário do Luvit — confirme com luvit -v) e cai para uma implementação SHA-256 pure-Lua automaticamente caso contrário, então funciona sem dependências extras de qualquer forma.

Configurar JWT Secret

Adicione no seu .env:

JWT_SECRET=sua_chave_secreta_super_segura_com_64_ou_mais_caracteres

Gerar Token

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

-- Payload do token
local payload = {
    user_id = 1,
    username = "joao",
    email = "joao@example.com",
    roles = {"user", "admin"}
}

-- Gerar token (expira em 15 minutos por padrão)
local token = jwt.sign(payload, os.getenv('JWT_SECRET'), {
    expiresIn = 900  -- 15 minutos em segundos
})

-- Retornar para o cliente
return ctx.json(200, {
    token = token,
    type = "Bearer"
})

Verificar Token

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

-- Obter token do header Authorization
local token = ctx.getBearer()  -- Remove "Bearer " automaticamente

-- Verificar e decodificar
local ok, payload_or_error = jwt.verify(token, os.getenv('JWT_SECRET'))

if ok then
    -- Token válido
    local user_id = payload_or_error.user_id
    local username = payload_or_error.username
    -- ... usar dados
else
    -- Token inválido
    return ctx.error(401, payload_or_error)
end

Opções Avançadas

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

-- Token com claims adicionais
local token = jwt.sign(payload, secret, {
    expiresIn = 3600,           -- Expira em 1 hora
    notBefore = 0,              -- Válido imediatamente
    issuer = "crescent-app",    -- Quem emitiu
    audience = "api-users"      -- Para quem é destinado
})

-- Verificar com validação de claims
local ok, payload = jwt.verify(token, secret, {
    issuer = "crescent-app",    -- Valida issuer
    audience = "api-users"      -- Valida audience
})

Access Token e Refresh Token

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

local payload = {
    user_id = 1,
    username = "joao"
}

-- Access token (curta duração - 15 min)
local access_token = jwt.create_access_token(
    payload, 
    os.getenv('JWT_SECRET'),
    900  -- 15 minutos (opcional, padrão já é 15min)
)

-- Refresh token (longa duração - 30 dias)
local refresh_token = jwt.create_refresh_token(
    payload,
    os.getenv('JWT_SECRET'),
    2592000  -- 30 dias (opcional, padrão já é 30 dias)
)

return ctx.json(200, {
    access_token = access_token,
    refresh_token = refresh_token,
    token_type = "Bearer",
    expires_in = 900
})

Decodificar Sem Verificar

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

-- Apenas para debug/inspeção - NÃO use para autenticação!
local header, payload = jwt.decode(token)

print("Algorithm:", header.alg)  -- "HS256"
print("User ID:", payload.user_id)
-- ⚠️ Assinatura NÃO foi verificada!

Middleware de Autenticação JWT

Middlewares no Crescent são sempre globais (app:use(middleware), sem segundo argumento de path) — não existe middleware escopado por rota. Para proteger só parte das rotas, registre o middleware depois das rotas públicas e antes das rotas protegidas, ou monte um sub-app/roteador separado por módulo.

local auth = require('crescent.middleware.auth')

-- Middleware JWT básico (protege tudo que for registrado depois dele)
app:use(auth.jwt())

-- Com opções customizadas
app:use(auth.jwt({
    secret = os.getenv('JWT_SECRET'),
    issuer = "crescent-app",
    audience = "admin-panel",
    getUserFromPayload = function(payload, ctx)
        -- Buscar usuário completo do banco
        return User:find(payload.user_id)
    end
}))

-- Usar dados do usuário na rota
app:get('/api/profile', function(ctx)
    -- ctx.state.user foi populado pelo middleware
    local user = ctx.state.user
    return ctx.json(200, {
        id = user.id,
        name = user.name,
        email = user.email
    })
end)

Helpers do Middleware Auth

local auth = require('crescent.middleware.auth')

-- Gerar token manualmente
local token = auth.generate_token({
    user_id = 1,
    username = "joao"
}, {
    secret = os.getenv('JWT_SECRET'),
    expiresIn = 3600
})

-- Gerar par de tokens (access + refresh)
local tokens = auth.generate_token_pair({
    user_id = 1,
    username = "joao"
}, {
    secret = os.getenv('JWT_SECRET'),
    access_expires_in = 900,      -- 15 min
    refresh_expires_in = 2592000  -- 30 dias
})

-- tokens = {
--     access_token = "eyJ...",
--     refresh_token = "eyJ...",
--     token_type = "Bearer",
--     expires_in = 900
-- }

-- Verificar token fora do middleware
local ok, payload = auth.verify_token(token, {
    secret = os.getenv('JWT_SECRET')
})

-- Decodificar sem verificar (debug)
local header, payload = auth.decode_token(token)

Exemplo Completo: Sistema de Auth

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

local AuthService = {}

-- Registro de usuário
function AuthService.register(data)
    -- Validar dados
    if not data.email or not data.password then
        error("Email e senha são obrigatórios")
    end
    
    -- Hash da senha
    local passwordHash = hash.encrypt(data.password)
    
    -- Criar usuário
    local user = User:create({
        name = data.name,
        email = data.email,
        password = passwordHash
    })
    
    -- Gerar tokens
    local tokens = AuthService.generateTokens(user)
    
    return {
        user = {
            id = user.id,
            name = user.name,
            email = user.email
        },
        tokens = tokens
    }
end

-- Login de usuário
function AuthService.login(email, password)
    -- Buscar usuário (where() é posicional)
    local user = User:where("email", email):first()
    
    if not user then
        error("Credenciais inválidas")
    end
    
    -- Verificar senha
    if not hash.verify(password, user.password) then
        error("Credenciais inválidas")
    end
    
    -- Gerar tokens
    local tokens = AuthService.generateTokens(user)
    
    return {
        user = {
            id = user.id,
            name = user.name,
            email = user.email
        },
        tokens = tokens
    }
end

-- Refresh token
function AuthService.refresh(refresh_token)
    local secret = os.getenv('JWT_SECRET')
    
    -- Verificar refresh token
    local ok, payload = jwt.verify(refresh_token, secret)
    
    if not ok then
        error("Token inválido ou expirado")
    end
    
    -- Buscar usuário
    local user = User:find(payload.user_id)
    
    if not user then
        error("Usuário não encontrado")
    end
    
    -- Gerar novo access token
    local access_token = jwt.create_access_token({
        user_id = user.id,
        username = user.name,
        email = user.email
    }, secret)
    
    return {
        access_token = access_token,
        token_type = "Bearer",
        expires_in = 900
    }
end

-- Helper para gerar tokens
function AuthService.generateTokens(user)
    local secret = os.getenv('JWT_SECRET')
    
    local payload = {
        user_id = user.id,
        username = user.name,
        email = user.email
    }
    
    local access_token = jwt.create_access_token(payload, secret)
    local refresh_token = jwt.create_refresh_token(payload, secret)
    
    return {
        access_token = access_token,
        refresh_token = refresh_token,
        token_type = "Bearer",
        expires_in = 900
    }
end

return AuthService

Rotas de Autenticação

-- src/auth/routes/auth.lua
local AuthService = require('src.auth.services.auth')
local auth = require('crescent.middleware.auth')

return function(app)
    -- Registro
    app:post('/auth/register', function(ctx)
        local data = ctx.body
        
        local ok, result = pcall(function()
            return AuthService.register(data)
        end)
        
        if not ok then
            return ctx.json(400, {error = result})
        end
        
        return ctx.json(201, result)
    end)
    
    -- Login
    app:post('/auth/login', function(ctx)
        local data = ctx.body
        
        local ok, result = pcall(function()
            return AuthService.login(data.email, data.password)
        end)
        
        if not ok then
            return ctx.json(401, {error = result})
        end
        
        return ctx.json(200, result)
    end)
    
    -- Refresh token
    app:post('/auth/refresh', function(ctx)
        local refresh_token = ctx.body.refresh_token
        
        if not refresh_token then
            return ctx.json(400, {error = "Refresh token é obrigatório"})
        end
        
        local ok, result = pcall(function()
            return AuthService.refresh(refresh_token)
        end)
        
        if not ok then
            return ctx.json(401, {error = result})
        end
        
        return ctx.json(200, result)
    end)
end

-- Rotas protegidas (registre o middleware antes de montar essas rotas
-- no app.lua, já que app:use() é sempre global):
--
--   app:use(auth.jwt())
--   app:get('/auth/profile', function(ctx) ... end)

Claims Padrão JWT

Claim Descrição Exemplo
iat Issued At - quando foi criado 1705484400
exp Expiration - quando expira 1705488000
nbf Not Before - válido a partir de 1705484400
iss Issuer - quem emitiu "crescent-app"
aud Audience - para quem é destinado "api-users"

Boas Práticas JWT

Recomendado:

  • Use secrets longos e aleatórios (64+ caracteres)
  • Access tokens curtos (15 min)
  • Refresh tokens longos (30 dias)
  • Armazene tokens com segurança no cliente (HttpOnly cookies)
  • Valide claims como issuer e audience
  • Implemente refresh token rotation

Evite:

  • Armazenar dados sensíveis no payload (é decodificável!)
  • Tokens muito longos (> 7 dias para access)
  • Reutilizar JWT secret entre ambientes
  • Esquecer de validar expiração
  • Confiar apenas no token sem validar usuário no banco

Testar JWT

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

local secret = "test_secret_key"

local jwtTests = {
    testSignAndVerify = function()
        local payload = {user_id = 1}
        local token = jwt.sign(payload, secret)
        
        local ok, decoded = jwt.verify(token, secret)
        tests.assertTrue(ok)
        tests.assertEquals(decoded.user_id, 1)
    end,
    
    testInvalidSignature = function()
        local payload = {user_id = 1}
        local token = jwt.sign(payload, secret)
        
        local ok, error = jwt.verify(token, "wrong_secret")
        tests.assertFalse(ok)
        tests.assertNotNil(error)
    end,
    
    testExpiration = function()
        local payload = {user_id = 1}
        local token = jwt.sign(payload, secret, {expiresIn = 1})
        
        local ok = jwt.verify(token, secret)
        tests.assertTrue(ok)
        -- Token expira após 1 segundo
    end
}

tests.runSuite("JWT Tests", jwtTests)

📧 Email

crescent.utils.mail envia email via SMTP, construído sobre socket.smtp/ltn12/mime (dependência LuaRocks: luasocket). As credenciais padrão vêm do .env (SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_FROM_NAME).

# .env
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=seu_email@gmail.com
SMTP_PASSWORD=sua_senha_de_app
SMTP_FROM=seu_email@gmail.com
SMTP_FROM_NAME="Minha Aplicação"

Enviar Email

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

-- Texto simples
local result, err = mail.send_text(
    "destinatario@example.com",
    "Bem-vindo!",
    "Obrigado por se cadastrar."
)

-- HTML (com fallback em texto opcional)
local result, err = mail.send_html(
    "destinatario@example.com",
    "Bem-vindo!",
    "<h1>Bem-vindo!</h1><p>Seu cadastro foi confirmado.</p>",
    "Bem-vindo! Seu cadastro foi confirmado."
)

-- Opções completas (to/cc/bcc aceitam string, array de strings, ou
-- array de {email=..., name=...})
local result, err = mail.send({
    to = { { email = "user@example.com", name = "Usuário" } },
    cc = "outro@example.com",
    subject = "Relatório mensal",
    html = "<h1>Relatório</h1>",
    reply_to = "suporte@example.com"
})

if not result then
    print("Falha ao enviar:", err)
end

Template (etlua)

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

-- Renderiza o .etlua com etlua.render, envia como HTML e gera
-- automaticamente uma versão em texto puro (strip de tags)
local result, err = mail.send_template(
    "destinatario@example.com",
    "Recuperação de senha",
    "views/emails/reset-password.etlua",
    { reset_link = "https://app.com/reset?token=abc123" }
)

Instância customizada e verificação de conexão

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

local custom_mailer = mail.create({
    server = "smtp.outro-provedor.com",
    port = 465,
    user = "outro@example.com",
    password = "..."
})

-- Testa conectividade TCP com o servidor SMTP (não envia email)
local ok, msg = mail.verify()

🌐 Variáveis de Ambiente

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

-- Carregar .env (usado automaticamente por env.get() na primeira chamada)
env.load('.env')

-- Obter valores, com fallback opcional
local dbHost = env.get('DB_HOST', 'localhost')
local port = env.get('APP_PORT')
local isDev = env.get('APP_ENV') == 'development'

-- Limpar cache (útil em testes que trocam variáveis em runtime)
env.clear_cache()

Não existe env.has(...) — para checar presença, compare com nil: if env.get('API_KEY') then ... end.


📨 Headers HTTP

crescent.utils.headers normaliza headers de requisição (não é um parser de headers de resposta genérico).

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

-- normalize(req) recebe o OBJETO de requisição inteiro (req.rawHeaders /
-- req.headers), não um nome de header — devolve uma tabela com todos os
-- headers em lowercase: { authorization = "...", ["content-type"] = "..." }
local normalized = headers.normalize(req)

-- Extrai o token de um header "Authorization: Bearer <token>" já normalizado
local token = headers.get_bearer(normalized)

⚠️ headers.is_safe_value() está quebrado hoje (mesma causa de stringUtil.is_safe()/sanitize(), ver seção "String Utilities" abaixo): o pattern [\r\n\0] inclui um byte nulo dentro de uma character class, o que lança malformed pattern (missing ']') em qualquer chamada neste Luvit/LuaJIT, independente do conteúdo testado. Bug de código, reportado aqui, não corrigido.

Na prática, você raramente chama headers.normalize() direto — o crescent.core.context já expõe o resultado normalizado em ctx.headers, e ctx.getHeader(name) / ctx.getBearer() fazem a leitura pra você:

local auth_header = ctx.getHeader("authorization")
local token = ctx.getBearer()

🛤️ Path Utilities

crescent.utils.path foi feito para paths de rota HTTP, não para manipulação de paths de arquivo do sistema operacional (não existe dirname/basename/extname).

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

-- Junta dois segmentos de path (só 2 argumentos)
local fullPath = pathUtil.join('/api', 'users')
-- "/api/users"

-- Normaliza: colapsa "//" repetidos e garante "/" inicial
-- (NÃO resolve ".."/"." — isso é tratado por is_safe, não normalize)
local normalized = pathUtil.normalize('api//users')
-- "/api/users"

-- Valida se o path é seguro (sem ".." nem null byte) — usado
-- internamente pelo middleware de arquivos estáticos contra path traversal
local safe = pathUtil.is_safe('/../../etc/passwd')  -- false
local safe2 = pathUtil.is_safe('/css/app.css')       -- true

-- Compila um template de rota "/user/{id}" em pattern Lua + nomes de
-- parâmetros — usado internamente pelo roteador
local pattern, names = pathUtil.compile('/user/{id}')
-- pattern: "^/user/?([^/]*)$"; names: {"id"}

🔤 String Utilities

crescent.utils.string foca em segurança (sanitização/validação), não em manipulação geral de strings — não existem split/startsWith/ endsWith/upper/lower/titleCase/slugify.

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

-- Remove espaços do início/fim
local trimmed = stringUtil.trim('  hello  ')  -- "hello"

-- Escapa metacaracteres de pattern Lua (evita injeção de pattern em
-- gsub/match/find quando o texto vem de input externo)
local safe_pattern = stringUtil.escape_lua_pattern('1.99 (promo)')
-- "1%.99 %(promo%)"

-- Limita o tamanho (proteção contra payloads gigantes / DoS)
local limited = stringUtil.limit(long_string, 8192)  -- default 8192

Para split/case/slugify, use as primitivas nativas do Lua (string.gmatch, string.upper/lower, string.gsub) diretamente — não há um wrapper do Crescent pra isso hoje.

⚠️ is_safe() e sanitize() estão quebrados hoje. Ambos usam o pattern [\0-\8\11-\12\14-\31\127] (byte nulo como início de um range de character class) — testado neste Luvit/LuaJIT, isso lança malformed pattern (missing ']') em qualquer chamada, mesmo com entrada sem byte nulo (o erro é no parsing do pattern, não no conteúdo). É um bug no código-fonte (crescent/utils/string.lua), não um erro de documentação — reportado, não corrigido aqui. Enquanto não for corrigido, não use is_safe()/sanitize(); escape_lua_pattern() e limit() continuam funcionando normalmente.


🧪 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,
    
    testStringEscapePattern = function()
        tests.assertEquals(stringUtil.escape_lua_pattern("1.99"), "1%.99")
    end,

    testStringLimit = function()
        tests.assertEquals(#stringUtil.limit(string.rep("a", 20), 5), 5)
        tests.assertEquals(stringUtil.limit("hello", 8192), "hello")
    end
    -- stringUtil.is_safe()/sanitize() não entram aqui: estão quebrados
    -- hoje (ver seção "String Utilities" acima), passariam pra sempre
    -- lançar erro em vez de rodar a asserção
}

tests.runSuite("Utility Tests", utilTests)

🎨 Templates e Views (etlua)

O Crescent inclui suporte a templates usando etlua (Embedded Lua), permitindo criar aplicações MVC.

Sintaxe Básica

<!-- Variáveis -->
<h1>Olá, <%= name %>!</h1>

<!-- Condicionais -->
<% if user.admin then %>
    <p>Você é admin</p>
<% end %>

<!-- Loops -->
<ul>
<% for i, item in ipairs(items) do %>
    <li><%= item.name %></li>
<% end %>
</ul>

Renderizar Views no Controller

local function show_profile(ctx)
    local user = User:find(ctx.params.id)
    
    -- Renderiza view com dados
    return ctx.view("views/profile.etlua", {
        name = user.name,
        email = user.email,
        created_at = user.created_at
    })
end

Renderizar Template Direto

local etlua = require("crescent.utils.etlua")

-- String
local html = etlua.render("Olá, <%= name %>!", { name = "João" })

-- Arquivo
local html, err = etlua.render_file("views/home.etlua", {
    title = "Home",
    users = users_list
})

if not html then
    print("Erro: " .. err)
end

Tags Disponíveis

  • <% código %> - Executa código Lua (sem output)
  • <%= variável %> - Exibe valor (com escape HTML automático)
  • <%- variável %> - Exibe valor (SEM escape HTML)
  • <% código -%> - Remove quebra de linha após a tag

Exemplo Completo

Controller (src/users/controllers/users.lua):

local User = require("src.users.models.users")

local function list(ctx)
    local users = User:all()
    
    return ctx.view("views/users/list.etlua", {
        users = users,
        total = #users
    })
end

local function show(ctx)
    local user = User:find(ctx.params.id)
    
    if not user then
        return ctx.error(404, "Usuário não encontrado")
    end
    
    return ctx.view("views/users/show.etlua", {
        name = user.name,
        email = user.email,
        role = user.role
    })
end

return {
    list = list,
    show = show
}

View (views/users/list.etlua):

<!DOCTYPE html>
<html>
<head>
    <title>Lista de Usuários</title>
</head>
<body>
    <h1>Usuários (<%= total %>)</h1>
    
    <% if total > 0 then %>
        <table>
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Nome</th>
                    <th>Email</th>
                </tr>
            </thead>
            <tbody>
                <% for i, user in ipairs(users) do %>
                <tr>
                    <td><%= user.id %></td>
                    <td><%= user.name %></td>
                    <td><%= user.email %></td>
                </tr>
                <% end %>
            </tbody>
        </table>
    <% else %>
        <p>Nenhum usuário encontrado.</p>
    <% end %>
</body>
</html>

Passando Funções para Views

return ctx.view("views/dashboard.etlua", {
    users = users,
    format_date = function(timestamp)
        return os.date("%d/%m/%Y", timestamp)
    end
})

Usando na view:

<p>Data: <%= format_date(os.time()) %></p>

Tratamento de Erros

local etlua = require("crescent.utils.etlua")

local html, err = etlua.render_file("views/my_view.etlua", data)

if not html then
    print("Erro ao renderizar: " .. err)
    return ctx.html(500, "<h1>Erro ao carregar página</h1>")
end

return ctx.html(200, html)

💡 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('APP_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

Guia completo para colocar sua aplicação Crescent em produção.

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

O Crescent Framework já vem com um config/nginx.conf de referência bem mais completo que o exemplo básico acima — com rate limiting por zona (limit_req_zone), cache de assets estáticos, headers de segurança e bloqueio de scanners comuns (/wp-admin, /phpmyadmin, etc). Vale usar ele como ponto de partida em vez do exemplo mínimo acima; a seção SSL abaixo já reflete a versão com SSL desse arquivo.


🔒 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

Esta é a versão adaptada do config/nginx.conf que já vem no Crescent Framework — troque yourdomain.com pelo seu domínio real:

# /etc/nginx/sites-available/meu-app

upstream crescent_backend {
    server 127.0.0.1:8080;
    keepalive 64;
}

# Rate limiting zones
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/s;

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 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;

    # Headers de segurança
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

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

    client_max_body_size 10M;

    # Rotas normais
    location / {
        limit_req zone=general_limit burst=20 nodelay;
        limit_req_status 429;

        proxy_pass http://crescent_backend;
        proxy_http_version 1.1;

        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_set_header Connection "";

        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Rotas de API — limite mais permissivo
    location /api/ {
        limit_req zone=api_limit burst=30 nodelay;
        limit_req_status 429;

        proxy_pass http://crescent_backend;
        proxy_http_version 1.1;
        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_set_header Connection "";
    }

    # Rotas de auth (login/register) — limite restritivo
    location ~ ^/(login|register|auth)/ {
        limit_req zone=auth_limit burst=5 nodelay;
        limit_req_status 429;

        proxy_pass http://crescent_backend;
        proxy_http_version 1.1;
        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;
    }

    # /health sem rate limit (usado pelo monitoramento externo, ver seção abaixo)
    location /health {
        access_log off;
        proxy_pass http://crescent_backend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Connection "";
    }

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

    # Bloqueia acesso a arquivos ocultos, backups e scanners comuns
    location ~ /\. { deny all; access_log off; log_not_found off; }
    location ~ ~$ { deny all; access_log off; log_not_found off; }
    location ~ /(wp-admin|wp-login|phpmyadmin|admin) { deny all; access_log off; log_not_found off; }
}

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"

# app.lua é o entrypoint real (faz require("./bootstrap") e chama
# app:listen()); bootstrap.lua sozinho só ajusta package.path e não sobe
# servidor nenhum — apontar o ExecStart pra ele faz o serviço encerrar
# imediatamente e ficar em loop de restart.
ExecStart=/home/www-data/.local/bin/luvit app.lua

Restart=always
RestartSec=10
StartLimitIntervalSec=60
StartLimitBurst=3
StandardOutput=journal
StandardError=journal
SyslogIdentifier=meu-app

# Recursos (opcional)
MemoryLimit=512M
CPUQuota=50%

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

[Install]
WantedBy=multi-user.target

Isso corresponde ao config/crescent.service que já vem no Crescent Framework, com duas correções: o ExecStart de lá aponta pra um example.lua que não existe no repo (deveria ser app.lua, igual acima), e ele define tanto APP_ENV quanto NODE_ENV (resíduo de outro stack) — o framework só lê APP_ENV.

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

Em app.lua, guarde o horário de início antes de app:listen():

-- app.lua
_G.APP_START_TIME = os.time()

O arquivo de rotas segue a mesma convenção usada por make:routes/make:module (function(app, prefix), não um objeto "router" separado):

-- src/health/routes/health.lua
return function(app, prefix)
    app:get(prefix or "/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"),
            -- os.clock() mede CPU consumida pelo processo, não tempo de
            -- parede — para uptime real, compare contra o horário salvo
            -- na subida do servidor
            uptime_seconds = os.time() - (_G.APP_START_TIME or os.time()),
            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