🌙 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.zipO que é Crescent?
Crescent é um framework web completo que combina a performance do Lua/LuaJIT com uma arquitetura moderna e modular inspirada em frameworks como NestJS e Laravel. Foi projetado para criar APIs REST e aplicações web de alto desempenho com código limpo e organizado.
🌟 Principais Características
- ⚡ Performance: Built on LuaJIT + libuv (Luvit) for blazing fast execution
- 🎯 Modular: Organize código em módulos independentes e reutilizáveis
- 🗄️ ORM ActiveRecord: Interaja com banco de dados MySQL de forma elegante
- 🔄 Migrations: Sistema completo de versionamento de schema
- 🛠️ CLI Poderoso: Geração automática de código (como Artisan do Laravel)
- 🔐 Segurança: Middleware de segurança, validações e hash de senhas PBKDF2
- ✅ Testes: Biblioteca completa de assertions para testes automatizados
- 📦 Pronto para Produção: Configuração NGINX e systemd incluídas
📦 Instalação Rápida
Método 1: Download Direto (Recomendado)
Baixe o template starter pronto para uso:
Método 1: Via Lit Package Manager
# Instale o framework via Lit
lit install daniel-m-tfs/crescent-framework
# Clone o starter template
git clone https://github.com/daniel-m-tfs/crescent-starter.git meu-projeto
cd meu-projeto
# Configure e inicie
cp .env.example .env
nano .env
luvit app.lua
Método 2: CLI (Criar Novo Projeto)
# Se você tem o CLI instalado globalmente
crescent new meu-projeto
cd meu-projeto
cp .env.example .env
nano .env
luvit app.lua
📚 Dependências
Requisitos do Sistema
- Luvit 2.18+: Runtime Lua assíncrono
- Lit 3.8+: Gerenciador de pacotes
- MySQL 5.7+ ou MariaDB 10+: Banco de dados
- Linux/macOS: Sistema operacional (Windows via WSL)
Instalando Luvit e Lit
# macOS via Homebrew
brew install luvit
# Linux - Download manual
curl -L https://github.com/luvit/lit/raw/master/get-lit.sh | sh
Dependências do Framework
O Crescent Framework automaticamente inclui:
luvit/luvit@2.18.1- Runtime basecreationix/mysql- Driver MySQL (instalar separadamente)
Para instalar o driver MySQL:
lit install creationix/mysql
📁 Estrutura do Projeto
crescent-starter/
├── app.lua # 🚀 Arquivo principal da aplicação
├── bootstrap.lua # 🔧 Bootstrap do framework
├── crescent-cli.lua # 🛠️ CLI para geração de código
├── .env # 🔐 Variáveis de ambiente (não versionar!)
├── .env.example # 📝 Template de variáveis
├── config/
│ ├── development.lua # ⚙️ Config de desenvolvimento
│ ├── production.lua # ⚙️ Config de produção
│ ├── nginx.conf # 🌐 Configuração NGINX
│ └── crescent.service # 🔄 Systemd service
├── crescent/ # 📦 Core do framework
│ ├── init.lua
│ ├── server.lua
│ ├── core/
│ │ ├── context.lua # Contexto HTTP (req/res)
│ │ ├── request.lua # Request object
│ │ ├── response.lua # Response object
│ │ └── router.lua # Sistema de rotas
│ ├── database/
│ │ ├── model.lua # ORM ActiveRecord
│ │ ├── query_builder.lua # Query Builder
│ │ ├── mysql.lua # Driver MySQL
│ │ └── migrate.lua # Sistema de migrations
│ ├── middleware/
│ │ ├── auth.lua # Autenticação
│ │ ├── cors.lua # CORS
│ │ ├── logger.lua # Logging
│ │ └── security.lua # Segurança
│ └── utils/
│ ├── env.lua # Variáveis de ambiente
│ ├── hash.lua # 🔐 Hash de senhas PBKDF2
│ ├── tests.lua # ✅ Biblioteca de testes
│ ├── headers.lua # HTTP headers
│ ├── path.lua # Path utilities
│ └── string.lua # String utilities
├── src/ # 📝 Seu código (módulos)
│ └── users/ # Exemplo de módulo
│ ├── init.lua # Registrador do módulo
│ ├── controllers/
│ │ └── users.lua
│ ├── services/
│ │ └── users.lua
│ ├── models/
│ │ └── users.lua
│ └── routes/
│ └── users.lua
├── migrations/ # 🔄 Database migrations
│ └── 20260108230701_create_users_table.lua
└── tests/ # ✅ Testes automatizados
└── test-*.lua
🎯 Convenções de Diretórios
src/: Todos os seus módulos de negóciocrescent/: Core do framework (não modificar)config/: Arquivos de configuraçãomigrations/: Versionamento do banco de dadostests/: Testes automatizados
🔧 Configuração Inicial
1. Variáveis de Ambiente (.env)
# Ambiente
ENV=development
# Banco de Dados
DB_HOST=localhost
DB_PORT=3306
DB_NAME=meu_banco
DB_USER=root
DB_PASSWORD=senha_segura
# Servidor
PORT=8080
HOST=0.0.0.0
2. Testar Conexão MySQL
-- teste-conexao.lua
local MySQL = require('crescent.database.mysql')
MySQL:test()
luvit teste-conexao.lua
3. Criar Primeira Migration
luvit crescent-cli make:migration create_products_table
4. Executar Migrations
luvit crescent-cli migrate
🎮 Primeiro Módulo
Crie um módulo CRUD completo com um único comando:
luvit crescent-cli make:module Product
Isso cria:
- ✅ Controller (
src/product/controllers/product.lua) - ✅ Service (
src/product/services/product.lua) - ✅ Model (
src/product/models/product.lua) - ✅ Routes (
src/product/routes/product.lua) - ✅ Module Init (
src/product/init.lua)
Registrar Módulo no app.lua
-- app.lua
local Crescent = require('crescent')
local app = Crescent.create()
-- Registra módulo Product
local ProductModule = require("src.product")
ProductModule.register(app)
app:listen(8080, function()
print("✓ Servidor rodando em http://localhost:8080")
end)
🚀 Iniciando o Servidor
Modo Desenvolvimento
luvit app.lua
Ou use o CLI:
luvit crescent-cli server
Acessar API
# Listar produtos
curl http://localhost:8080/product
# Criar produto
curl -X POST http://localhost:8080/product \
-H "Content-Type: application/json" \
-d '{"name":"Notebook","price":2500}'
# Buscar por ID
curl http://localhost:8080/product/1
# Atualizar
curl -X PUT http://localhost:8080/product/1 \
-H "Content-Type: application/json" \
-d '{"name":"Notebook Dell","price":2800}'
# Deletar
curl -X DELETE http://localhost:8080/product/1
🧪 Executando Testes
# Rodar todos os testes
luvit crescent-cli test
# Rodar teste específico
luvit tests/test-users.lua
📖 Próximos Passos
Agora que você tem um projeto rodando, explore:
- CLI - Aprenda todos os comandos disponíveis
- Core Concepts - Rotas, Controllers, Services
- Database & ORM - Modelos, relações, migrations
- Utilities - Testes, hash, helpers
- 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"
- Verifique se MySQL está rodando:
mysql.server status - Teste credenciais:
mysql -u root -p - Confira
.env:DB_HOST,DB_USER,DB_PASSWORD
Porta já em uso
# Mate processo na porta 8080
lsof -ti:8080 | xargs kill -9
# Ou mude a porta no .env
PORT=3000
📚 Recursos Adicionais
🛣️ Rotas
Rotas definem os endpoints da sua API e conectam URLs aos controllers.
Definindo Rotas Básicas
-- app.lua
local Crescent = require('crescent')
local app = Crescent.create()
-- GET
app:get('/hello', function(ctx)
return ctx.json(200, { message = "Hello World" })
end)
-- POST
app:post('/users', function(ctx)
local body = ctx.body
return ctx.json(201, body)
end)
-- PUT
app:put('/users/{id}', function(ctx)
local id = ctx.params.id
return ctx.json(200, { id = id })
end)
-- DELETE
app:delete('/users/{id}', function(ctx)
local id = ctx.params.id
return ctx.no_content()
end)
app:listen(8080)
Parâmetros de Rota
-- Parâmetro único
app:get('/users/{id}', function(ctx)
local id = ctx.params.id
return ctx.json(200, { userId = id })
end)
-- Múltiplos parâmetros
app:get('/posts/{postId}/comments/{commentId}', function(ctx)
local postId = ctx.params.postId
local commentId = ctx.params.commentId
return ctx.json(200, { postId = postId, commentId = commentId })
end)
Query Parameters
app:get('/search', function(ctx)
local query = ctx.query.q
local page = ctx.query.page or 1
local limit = ctx.query.limit or 10
return ctx.json(200, {
query = query,
page = tonumber(page),
limit = tonumber(limit)
})
end)
-- GET /search?q=lua&page=2&limit=20
Request Body
app:post('/users', function(ctx)
local body = ctx.body
-- Acessar campos
local name = body.name
local email = body.email
return ctx.json(201, {
name = name,
email = email
})
end)
Organização em Arquivos
-- src/users/routes/users.lua
local controller = require("src.users.controllers.users")
return function(app, prefix)
prefix = prefix or "/users"
app:get(prefix, function(ctx)
return controller:index(ctx)
end)
app:get(prefix .. "/{id}", function(ctx)
return controller:show(ctx)
end)
app:post(prefix, function(ctx)
return controller:create(ctx)
end)
app:put(prefix .. "/{id}", function(ctx)
return controller:update(ctx)
end)
app:delete(prefix .. "/{id}", function(ctx)
return controller:delete(ctx)
end)
end
Registrar Rotas no App
-- app.lua
local userRoutes = require("src.users.routes.users")
userRoutes(app, "/api/users")
🎮 Controllers
Controllers recebem requisições HTTP e retornam respostas. Devem ser finos e delegar lógica para services.
Estrutura Básica
-- src/users/controllers/users.lua
local service = require("src.users.services.users")
local UsersController = {}
function UsersController:index(ctx)
local users = service:getAll()
return ctx.json(200, users)
end
function UsersController:show(ctx)
local id = ctx.params.id
local user = service:getById(id)
if user then
return ctx.json(200, user)
end
return ctx.json(404, { error = "User not found" })
end
function UsersController:create(ctx)
local body = ctx.body or {}
-- Validação básica
if not body.name or not body.email then
return ctx.json(400, { error = "Name and email are required" })
end
local user = service:create(body)
return ctx.json(201, user)
end
function UsersController:update(ctx)
local id = ctx.params.id
local body = ctx.body or {}
local user = service:update(id, body)
if user then
return ctx.json(200, user)
end
return ctx.json(404, { error = "User not found" })
end
function UsersController:delete(ctx)
local id = ctx.params.id
local success = service:delete(id)
if success then
return ctx.no_content()
end
return ctx.json(404, { error = "User not found" })
end
return UsersController
Context Object (ctx)
O objeto ctx contém toda informação da requisição:
function Controller:example(ctx)
-- Parâmetros de rota
local id = ctx.params.id
-- Query parameters
local page = ctx.query.page
-- Request body
local body = ctx.body
-- Headers
local auth = ctx.headers['authorization']
-- Method
local method = ctx.method -- GET, POST, etc
-- Path
local path = ctx.path -- /users/123
-- Response helpers
return ctx.json(200, data)
return ctx.text(200, "Hello")
return ctx.html(200, "<h1>Hello</h1>")
return ctx.no_content() -- 204
return ctx.redirect(302, "/new-url")
end
Validação no Controller
function UsersController:create(ctx)
local body = ctx.body or {}
-- Validação manual
local errors = {}
if not body.name or body.name == "" then
table.insert(errors, "Name is required")
end
if not body.email or not string.match(body.email, ".+@.+%.%w+") then
table.insert(errors, "Valid email is required")
end
if #errors > 0 then
return ctx.json(422, { errors = errors })
end
-- Criar usuário
local user = service:create(body)
return ctx.json(201, user)
end
Error Handling
function UsersController:create(ctx)
local success, result = pcall(function()
return service:create(ctx.body)
end)
if not success then
-- Log error
print("Error creating user:", result)
return ctx.json(500, {
error = "Internal server error",
message = result
})
end
return ctx.json(201, result)
end
⚙️ Services
Services contêm a lógica de negócio da aplicação. Devem ser independentes de HTTP.
Estrutura Básica
-- src/users/services/users.lua
local UsersService = {}
local User = require("src.users.models.user")
function UsersService:getAll()
return User:all()
end
function UsersService:getById(id)
return User:find(id)
end
function UsersService:create(data)
-- Validação adicional
if not data.name or #data.name < 3 then
error("Name must be at least 3 characters")
end
return User:create(data)
end
function UsersService:update(id, data)
local user = User:find(id)
if not user then
return nil
end
user:update(data)
return user
end
function UsersService:delete(id)
local user = User:find(id)
if not user then
return false
end
user:delete()
return true
end
return UsersService
Lógica de Negócio Complexa
-- src/orders/services/orders.lua
local OrdersService = {}
local Order = require("src.orders.models.order")
local Product = require("src.products.models.product")
local User = require("src.users.models.user")
function OrdersService:createOrder(userId, items)
-- Validar usuário
local user = User:find(userId)
if not user then
error("User not found")
end
-- Validar produtos e calcular total
local total = 0
local validatedItems = {}
for _, item in ipairs(items) do
local product = Product:find(item.productId)
if not product then
error("Product " .. item.productId .. " not found")
end
if product.stock < item.quantity then
error("Insufficient stock for " .. product.name)
end
local subtotal = product.price * item.quantity
total = total + subtotal
table.insert(validatedItems, {
product_id = product.id,
quantity = item.quantity,
price = product.price,
subtotal = subtotal
})
end
-- Criar pedido
local order = Order:create({
user_id = userId,
total = total,
status = "pending"
})
-- Adicionar itens ao pedido
for _, item in ipairs(validatedItems) do
order:addItem(item)
-- Atualizar estoque
local product = Product:find(item.product_id)
product:update({
stock = product.stock - item.quantity
})
end
return order
end
function OrdersService:cancelOrder(orderId)
local order = Order:find(orderId)
if not order then
error("Order not found")
end
if order.status ~= "pending" then
error("Only pending orders can be cancelled")
end
-- Devolver produtos ao estoque
local items = order:items()
for _, item in ipairs(items) do
local product = Product:find(item.product_id)
product:update({
stock = product.stock + item.quantity
})
end
-- Atualizar status
order:update({ status = "cancelled" })
return order
end
return OrdersService
Services com Transações
function OrdersService:processPayment(orderId, paymentData)
local db = require('crescent.database.mysql')
-- Iniciar transação
db:query("START TRANSACTION")
local success, err = pcall(function()
local order = Order:find(orderId)
if not order then
error("Order not found")
end
-- Processar pagamento (API externa)
local paymentResult = PaymentGateway:charge(paymentData)
if not paymentResult.success then
error("Payment failed: " .. paymentResult.error)
end
-- Atualizar pedido
order:update({
status = "paid",
payment_id = paymentResult.id
})
-- Enviar email de confirmação
EmailService:sendOrderConfirmation(order)
end)
if success then
db:query("COMMIT")
return true
else
db:query("ROLLBACK")
error(err)
end
end
💾 Models (Básico)
Models representam dados e interagem com o banco via ORM ActiveRecord.
Definição Básica
-- src/users/models/user.lua
local Model = require("crescent.database.model")
local User = Model:extend({
table = "users",
primary_key = "id",
timestamps = true,
fillable = {
"name",
"email",
"password"
},
hidden = {
"password"
},
validates = {
name = { required = true, min = 3, max = 100 },
email = { required = true, email = true, unique = true },
password = { required = true, min = 6 }
}
})
return User
Operações CRUD
-- CREATE
local user = User:create({
name = "John Doe",
email = "john@example.com",
password = "hashed_password"
})
-- READ
local user = User:find(1)
local users = User:all()
local user = User:where({email = "john@example.com"}):first()
-- UPDATE
user:update({name = "Jane Doe"})
-- DELETE
user:delete()
Para mais detalhes sobre Models, veja Database & ORM.
🔒 Middleware
Middleware processa requisições antes que cheguem aos controllers.
Middleware de Autenticação
-- crescent/middleware/auth.lua
local Auth = {}
function Auth.middleware(ctx, next)
local token = ctx.headers['authorization']
if not token then
return ctx.json(401, { error = "No token provided" })
end
-- Validar token
local user = validateToken(token)
if not user then
return ctx.json(401, { error = "Invalid token" })
end
-- Adicionar usuário ao contexto
ctx.user = user
-- Continuar para próximo middleware/controller
return next()
end
return Auth
Aplicar Middleware
-- app.lua
local auth = require('crescent.middleware.auth')
-- Global (todas as rotas)
app:use(auth.middleware)
-- Específico para rota
app:get('/protected', auth.middleware, function(ctx)
return ctx.json(200, { user = ctx.user })
end)
Middleware de Logging
-- crescent/middleware/logger.lua
local Logger = {}
function Logger.middleware(ctx, next)
local start = os.clock()
-- Log request
print(string.format("[%s] %s %s", os.date(), ctx.method, ctx.path))
-- Executar próximo
local response = next()
-- Log response time
local duration = (os.clock() - start) * 1000
print(string.format(" → %d (%dms)", response.status or 200, duration))
return response
end
return Logger
Middleware CORS
-- crescent/middleware/cors.lua
local CORS = {}
function CORS.middleware(ctx, next)
-- Adicionar headers CORS
ctx.headers['Access-Control-Allow-Origin'] = '*'
ctx.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
ctx.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
-- Handle preflight
if ctx.method == 'OPTIONS' then
return ctx.no_content()
end
return next()
end
return CORS
Ordem dos Middlewares
-- app.lua
local cors = require('crescent.middleware.cors')
local logger = require('crescent.middleware.logger')
local auth = require('crescent.middleware.auth')
-- Ordem importa!
app:use(cors.middleware) -- 1. CORS primeiro
app:use(logger.middleware) -- 2. Logging
app:use(auth.middleware) -- 3. Auth por último
-- Rotas
app:get('/api/users', usersController.index)
🏗️ Módulos
Módulos agrupam funcionalidades relacionadas (controllers, services, models, routes).
Estrutura de Módulo
src/users/
├── init.lua # Registrador do módulo
├── controllers/
│ └── users.lua
├── services/
│ └── users.lua
├── models/
│ └── user.lua
└── routes/
└── users.lua
Module Init
-- src/users/init.lua
local Module = {}
function Module.register(app)
-- Registrar rotas
local routes = require("src.users.routes.users")
routes(app, "/api/users")
print("✓ Módulo Users carregado")
end
return Module
Registrar no App
-- app.lua
local Crescent = require('crescent')
local app = Crescent.create()
-- Registrar módulos
local UsersModule = require("src.users")
local ProductsModule = require("src.products")
UsersModule.register(app)
ProductsModule.register(app)
app:listen(8080)
Gerar Módulo via CLI
luvit crescent-cli make:module Product
Cria toda a estrutura automaticamente!
📊 Fluxo de Requisição
Cliente HTTP
↓
[Middleware CORS]
↓
[Middleware Logger]
↓
[Middleware Auth]
↓
[Router] → Encontra rota
↓
[Controller] → Recebe requisição
↓
[Service] → Lógica de negócio
↓
[Model/ORM] → Banco de dados
↓
[Service] ← Retorna dados
↓
[Controller] ← Formata resposta
↓
Cliente HTTP ← JSON response
💡 Boas Práticas
Controllers
- ✅ Mantenha finos (thin controllers)
- ✅ Delegue lógica para services
- ✅ Valide entrada básica
- ✅ Trate erros com try/catch (pcall)
- ✅ Retorne status codes apropriados
Services
- ✅ Lógica de negócio aqui
- ✅ Independente de HTTP
- ✅ Reutilizável entre controllers
- ✅ Use transações quando necessário
- ✅ Valide regras de negócio
Rotas
- ✅ Use padrões RESTful
- ✅ Agrupe por prefixo (
/api/v1) - ✅ Organize em arquivos separados
- ✅ Use nomes descritivos
Middleware
- ✅ Ordem importa
- ✅ CORS primeiro
- ✅ Auth/validação depois
- ✅ Use
next()para continuar
🧪 Testando Componentes
-- tests/test-users.lua
local tests = require('crescent.utils.tests')
local UsersService = require('src.users.services.users')
local userTests = {
testCreate = function()
local user = UsersService:create({
name = "Test User",
email = "test@example.com"
})
tests.assertNotNil(user)
tests.assertEquals(user.name, "Test User")
end,
testValidation = function()
tests.assertError(function()
UsersService:create({ name = "AB" }) -- Too short
end, "at least 3 characters")
end
}
tests.runSuite("Users Service Tests", userTests)
📖 Próximas Seções
- Database & ORM - Models em detalhes
- Utilities - Testes e helpers
- CLI - Geradores de código
🚀 Configuração do Banco
.env
DB_HOST=localhost
DB_PORT=3306
DB_NAME=meu_banco
DB_USER=root
DB_PASSWORD=senha123
Testar Conexão
local MySQL = require('crescent.database.mysql')
MySQL:test()
💾 Models (ORM ActiveRecord)
Definição Completa
-- src/products/models/product.lua
local Model = require("crescent.database.model")
local Product = Model:extend({
-- Nome da tabela
table = "products",
-- Chave primária (padrão: "id")
primary_key = "id",
-- Timestamps automáticos (created_at, updated_at)
timestamps = true,
-- Soft deletes (deleted_at)
soft_deletes = false,
-- Campos que podem ser preenchidos em massa
fillable = {
"name",
"description",
"price",
"stock",
"category_id"
},
-- Campos escondidos em JSON (não serializar)
hidden = {
"deleted_at"
},
-- Campos protegidos (nunca preenchidos em massa)
guarded = {
"id",
"created_at",
"updated_at"
},
-- Validações
validates = {
name = {
required = true,
min = 3,
max = 255,
unique = true
},
price = {
required = true,
numeric = true,
min = 0
},
stock = {
numeric = true,
min = 0
},
category_id = {
exists = {table = "categories", column = "id"}
}
},
-- Relações
relations = {
category = {
type = "belongsTo",
model = "Category",
foreign_key = "category_id"
},
orders = {
type = "hasMany",
model = "OrderItem",
foreign_key = "product_id"
}
}
})
-- Métodos personalizados
function Product:isLowStock()
return self.stock < 10
end
function Product:applyDiscount(percentage)
self.price = self.price * (1 - percentage / 100)
self:save()
end
return Product
📝 CRUD Operations
CREATE
local Product = require('src.products.models.product')
-- Método 1: create()
local product = Product:create({
name = "Notebook Dell",
price = 2500,
stock = 10
})
-- Método 2: new() + save()
local product = Product:new({
name = "Mouse Logitech",
price = 50
})
product:save()
READ
-- Buscar por ID
local product = Product:find(1)
-- Todos os registros
local products = Product:all()
-- Com condições
local products = Product:where({category_id = 5}):get()
-- Primeiro resultado
local product = Product:where({name = "Notebook"}):first()
-- Ordenar
local products = Product:orderBy('price', 'DESC'):get()
-- Limitar
local products = Product:limit(10):get()
-- Paginação
local products = Product:paginate(10, 1) -- 10 por página, página 1
UPDATE
-- Método 1: Buscar e atualizar
local product = Product:find(1)
product:update({
price = 2300,
stock = 15
})
-- Método 2: Modificar e salvar
local product = Product:find(1)
product.price = 2300
product:save()
-- Método 3: Update direto
Product:where({id = 1}):update({price = 2300})
DELETE
-- Soft delete (se soft_deletes = true)
local product = Product:find(1)
product:delete()
-- Hard delete (força remoção permanente)
product:forceDelete()
-- Delete direto por condição
Product:where({stock = 0}):delete()
🔍 Query Builder
Condições WHERE
-- Igualdade simples
Product:where({category_id = 5}):get()
-- Múltiplas condições (AND)
Product:where({
category_id = 5,
stock = {">", 10}
}):get()
-- Operadores
Product:where({price = {">", 1000}}):get()
Product:where({stock = {"<=", 5}}):get()
Product:where({name = {"LIKE", "%Dell%"}}):get()
-- WHERE IN
Product:whereIn('category_id', {1, 2, 3}):get()
-- WHERE BETWEEN
Product:whereBetween('price', 1000, 5000):get()
-- WHERE NULL
Product:whereNull('deleted_at'):get()
Product:whereNotNull('discount'):get()
OR Conditions
Product:where({category_id = 5})
:orWhere({category_id = 10})
:get()
Ordenação
-- ASC
Product:orderBy('name'):get()
Product:orderBy('name', 'ASC'):get()
-- DESC
Product:orderBy('price', 'DESC'):get()
-- Múltiplas ordenações
Product:orderBy('category_id')
:orderBy('price', 'DESC')
:get()
Limit e Offset
-- LIMIT
Product:limit(10):get()
-- OFFSET
Product:offset(20):limit(10):get()
-- Paginação
Product:paginate(20, 2) -- 20 por página, página 2
Select
-- Selecionar campos específicos
Product:select('id', 'name', 'price'):get()
-- Com alias
Product:select('id', 'name', 'price as valor'):get()
Joins
-- INNER JOIN
Product:join('categories', 'products.category_id', 'categories.id')
:select('products.*', 'categories.name as category_name')
:get()
-- LEFT JOIN
Product:leftJoin('categories', 'products.category_id', 'categories.id'):get()
Agrupamento
-- GROUP BY
Product:select('category_id', 'COUNT(*) as total')
:groupBy('category_id')
:get()
-- HAVING
Product:select('category_id', 'AVG(price) as avg_price')
:groupBy('category_id')
:having('avg_price', '>', 1000)
:get()
Agregações
-- COUNT
local total = Product:count()
local inStock = Product:where({stock = {">", 0}}):count()
-- SUM
local totalValue = Product:sum('price')
-- AVG
local avgPrice = Product:avg('price')
-- MIN / MAX
local minPrice = Product:min('price')
local maxPrice = Product:max('price')
Raw Queries
-- SELECT raw
local products = Product:raw([[
SELECT * FROM products
WHERE price > ? AND stock > ?
]], {1000, 0})
-- INSERT raw
Product:raw([[
INSERT INTO products (name, price)
VALUES (?, ?)
]], {"Teclado", 150})
-- Com bindings para segurança (evita SQL injection)
local search = ctx.query.search
local products = Product:raw([[
SELECT * FROM products
WHERE name LIKE ?
]], {"%" .. search .. "%"})
✅ Validações
Validações Disponíveis
validates = {
-- Obrigatório
name = { required = true },
-- Tamanho string
name = { min = 3, max = 255 },
-- Numérico
price = { numeric = true },
-- Range numérico
stock = { min = 0, max = 9999 },
-- Email
email = { email = true },
-- Único na tabela
email = { unique = true },
-- Existe em outra tabela
category_id = {
exists = {
table = "categories",
column = "id"
}
},
-- Regex customizado
phone = { pattern = "^%d%d%d%-%d%d%d%d$" },
-- Valores permitidos
status = { in_array = {"pending", "paid", "shipped"} }
}
Validação Manual
-- No Model
function Product:validate()
local errors = {}
if not self.name or #self.name < 3 then
table.insert(errors, "Name must be at least 3 characters")
end
if self.price and self.price < 0 then
table.insert(errors, "Price cannot be negative")
end
if #errors > 0 then
error(table.concat(errors, ", "))
end
return true
end
Validação no Service
-- src/products/services/products.lua
function ProductService:create(data)
-- Validação customizada
if not data.price or data.price <= 0 then
error("Invalid price")
end
if data.stock and data.stock < 0 then
error("Stock cannot be negative")
end
-- ORM faz validações do Model automaticamente
return Product:create(data)
end
🔗 Relações
BelongsTo (N:1)
-- Product pertence a Category
local Product = Model:extend({
table = "products",
relations = {
category = {
type = "belongsTo",
model = "Category",
foreign_key = "category_id"
}
}
})
-- Uso
local product = Product:find(1)
local category = product:category() -- Busca a categoria
print(category.name)
HasMany (1:N)
-- Category tem muitos Products
local Category = Model:extend({
table = "categories",
relations = {
products = {
type = "hasMany",
model = "Product",
foreign_key = "category_id"
}
}
})
-- Uso
local category = Category:find(1)
local products = category:products() -- Array de produtos
for _, product in ipairs(products) do
print(product.name)
end
HasOne (1:1)
-- User tem um Profile
local User = Model:extend({
table = "users",
relations = {
profile = {
type = "hasOne",
model = "Profile",
foreign_key = "user_id"
}
}
})
-- Uso
local user = User:find(1)
local profile = user:profile()
print(profile.bio)
BelongsToMany (N:N)
-- Product pertence a muitos Tags (via pivot)
local Product = Model:extend({
table = "products",
relations = {
tags = {
type = "belongsToMany",
model = "Tag",
pivot_table = "product_tags",
foreign_key = "product_id",
related_key = "tag_id"
}
}
})
-- Uso
local product = Product:find(1)
local tags = product:tags()
for _, tag in ipairs(tags) do
print(tag.name)
end
Eager Loading
-- N+1 Problem (ruim)
local products = Product:all()
for _, product in ipairs(products) do
local category = product:category() -- Query por produto!
end
-- Eager Loading (bom)
local products = Product:with('category'):get()
for _, product in ipairs(products) do
print(product.category.name) -- Já carregado!
end
-- Múltiplas relações
local products = Product:with('category', 'tags'):get()
🔄 Migrations
Criar Migration
luvit crescent-cli make:migration create_products_table
Migration de Criação
-- migrations/20260109123456_create_products_table.lua
local Migration = {}
function Migration:up()
return [[
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0,
category_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_category (category_id),
INDEX idx_price (price),
FOREIGN KEY (category_id)
REFERENCES categories(id)
ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
]]
end
function Migration:down()
return [[
DROP TABLE IF EXISTS products;
]]
end
return Migration
Migration de Alteração
-- migrations/20260109134500_add_discount_to_products.lua
local Migration = {}
function Migration:up()
return [[
ALTER TABLE products
ADD COLUMN discount DECIMAL(5, 2) DEFAULT 0,
ADD COLUMN is_featured BOOLEAN DEFAULT FALSE;
CREATE INDEX idx_featured ON products(is_featured);
]]
end
function Migration:down()
return [[
ALTER TABLE products
DROP COLUMN discount,
DROP COLUMN is_featured;
DROP INDEX idx_featured ON products;
]]
end
return Migration
Executar Migrations
# Executar pendentes
luvit crescent-cli migrate
# Desfazer última
luvit crescent-cli migrate:rollback
# Status
luvit crescent-cli migrate:status
🪝 Hooks (Lifecycle Events)
Hooks Disponíveis
local Product = Model:extend({
table = "products",
hooks = {
before_save = function(self)
-- Antes de salvar (CREATE ou UPDATE)
print("Saving product:", self.name)
end,
after_save = function(self)
-- Depois de salvar
print("Product saved:", self.id)
end,
before_create = function(self)
-- Antes de criar
self.slug = slugify(self.name)
end,
after_create = function(self)
-- Depois de criar
print("New product created!")
end,
before_update = function(self)
-- Antes de atualizar
end,
after_update = function(self)
-- Depois de atualizar
end,
before_delete = function(self)
-- Antes de deletar
end,
after_delete = function(self)
-- Depois de deletar
end
}
})
Exemplo Prático
local Product = Model:extend({
table = "products",
hooks = {
before_save = function(self)
-- Gerar slug automaticamente
if self.name and not self.slug then
self.slug = self.name:lower():gsub("%s+", "-")
end
end,
before_delete = function(self)
-- Verificar se pode deletar
local orderCount = OrderItem:where({product_id = self.id}):count()
if orderCount > 0 then
error("Cannot delete product with existing orders")
end
end,
after_create = function(self)
-- Notificar sistema
EventBus:emit('product.created', self)
end
}
})
💡 Boas Práticas
Índices
-- Campos frequentemente buscados
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_status ON orders(status);
-- Chaves estrangeiras
CREATE INDEX idx_category_id ON products(category_id);
-- Compostos para queries complexas
CREATE INDEX idx_category_price ON products(category_id, price);
Transações
local db = require('crescent.database.mysql')
function OrderService:processOrder(orderData)
db:query("START TRANSACTION")
local success, err = pcall(function()
-- Criar pedido
local order = Order:create(orderData)
-- Atualizar estoque
for _, item in ipairs(orderData.items) do
local product = Product:find(item.product_id)
product:update({
stock = product.stock - item.quantity
})
end
-- Criar pagamento
Payment:create({order_id = order.id, ...})
end)
if success then
db:query("COMMIT")
return true
else
db:query("ROLLBACK")
error(err)
end
end
N+1 Problem
-- ❌ Ruim (N+1 queries)
local products = Product:all()
for _, product in ipairs(products) do
local category = product:category() -- +1 query por produto
end
-- ✅ Bom (2 queries)
local products = Product:with('category'):get()
for _, product in ipairs(products) do
print(product.category.name) -- Já carregado
end
Paginação
-- No controller
function ProductController:index(ctx)
local page = tonumber(ctx.query.page) or 1
local perPage = tonumber(ctx.query.per_page) or 20
local result = Product:paginate(perPage, page)
return ctx.json(200, {
data = result.data,
current_page = result.current_page,
total = result.total,
per_page = result.per_page,
last_page = result.last_page
})
end
🧪 Testando Database
-- tests/test-product.lua
local tests = require('crescent.utils.tests')
local Product = require('src.products.models.product')
local productTests = {
testCreate = function()
local product = Product:create({
name = "Test Product",
price = 100,
stock = 10
})
tests.assertNotNil(product)
tests.assertNotNil(product.id)
tests.assertEquals(product.name, "Test Product")
end,
testValidation = function()
tests.assertError(function()
Product:create({name = ""}) -- Empty name
end)
end,
testRelations = function()
local product = Product:find(1)
local category = product:category()
tests.assertNotNil(category)
tests.assertIsTable(category)
end
}
tests.runSuite("Product Model Tests", productTests)
📖 Próximas Seções
- Core Concepts - Controllers e Services
- Utilities - Testes e helpers
- Deployment - Deploy em produção
📋 Comandos Disponíveis
luvit crescent-cli <comando> [opções]
Comandos Principais
| Comando | Descrição | |---------|-----------| |new | Cria um novo projeto Crescent |
| server | Inicia o servidor de desenvolvimento |
| test | Executa todos os testes do projeto |
| make:controller | Gera um controller |
| make:service | Gera um service |
| make:model | Gera um model |
| make:routes | Gera arquivo de rotas |
| make:module | Gera módulo completo (CRUD) |
| make:migration | Cria uma migration |
| migrate | Executa migrations pendentes |
| migrate:rollback | Desfaz última migration |
| migrate:status | Mostra status das migrations |
🆕 Criar Novo Projeto
luvit crescent-cli new meu-projeto
- ✅ Clona o
crescent-starterdo GitHub - ✅ Remove histórico Git do template
- ✅ Inicializa novo repositório Git
- ✅ Configura estrutura completa
cd meu-projeto
cp .env.example .env
nano .env # Configure MySQL
luvit app.lua
🚀 Servidor de Desenvolvimento
luvit crescent-cli server
- ✅ Inicia aplicação com
luvit app.lua - ✅ Logs em tempo real
- ✅ Substituição de processo (
exec) para manter saída interativa - ✅ Verifica se
app.luaexiste antes de iniciar
entr:
find . -name "*.lua" | entr -r luvit crescent-cli server
✅ Executar Testes
luvit crescent-cli test
- 🔍 Descobre automaticamente diretórios
tests/outest/ - 🔍 Encontra todos arquivos
test.luaoutests.lua - ▶️ Executa cada teste sequencialmente
- 📊 Mostra saída completa de cada teste
- 📈 Apresenta resumo final com estatísticas
- ✅/❌ Feedback visual colorido com emojis
🌙 Executando Testes Crescent
ℹ Encontrados 3 arquivo(s) de teste
📄 Executando: tests/test-users.lua
────────────────────────────────────
✅ testCreate passed
✅ testUpdate passed
✅ testDelete passed
=== Results: 3/3 passed, 0 failed ===
════════════════════════════════════
🌙 Resumo dos Testes
✅ Todos os testes passaram! (3/3)
🏗️ Geradores de Código
Gerar Controller
luvit crescent-cli make:controller Product
# ou especificar módulo
luvit crescent-cli make:controller Product catalog
src/product/controllers/product.lua
Template gerado:
-- src/product/controllers/product.lua
local service = require("src.product.services.product")
local ProductController = {}
function ProductController:index(ctx)
local result = service:getAll()
return ctx.json(200, result)
end
function ProductController:show(ctx)
local id = ctx.params.id
local result = service:getById(id)
if result then
return ctx.json(200, result)
end
return ctx.json(404, { error = "Not found" })
end
function ProductController:create(ctx)
local body = ctx.body or {}
local result = service:create(body)
return ctx.json(201, result)
end
function ProductController:update(ctx)
local id = ctx.params.id
local body = ctx.body or {}
local result = service:update(id, body)
if result then
return ctx.json(200, result)
else
return ctx.json(404, { error = "Not found" })
end
end
function ProductController:delete(ctx)
local id = ctx.params.id
local success = service:delete(id)
if success then
return ctx.no_content()
else
return ctx.json(404, { error = "Not found" })
end
end
return ProductController
Gerar Service
luvit crescent-cli make:service Product
src/product/services/product.lua
Template gerado:
-- src/product/services/product.lua
local ProductService = {}
local Product = require("src.product.models.product")
function ProductService:getAll()
return Product:all()
end
function ProductService:getById(id)
return Product:find(id)
end
function ProductService:create(body)
return Product:create(body)
end
function ProductService:update(id, body)
local product = Product:find(id)
if product then
product:update(body)
return product
end
return nil
end
function ProductService:delete(id)
local product = Product:find(id)
if product then
product:delete()
return true
end
return false
end
return ProductService
Gerar Model
luvit crescent-cli make:model Product
src/product/models/product.lua
Template gerado:
-- src/product/models/product.lua
local Model = require("crescent.database.model")
local Product = Model:extend({
table = "product",
primary_key = "id",
timestamps = true,
soft_deletes = false,
fillable = {
"name",
},
hidden = {
-- "password"
},
guarded = {
-- "id", "created_at", "updated_at"
},
validates = {
name = {required = true, min = 3, max = 255},
},
relations = {
-- posts = {type = "hasMany", model = "Post", foreign_key = "user_id"},
}
})
return Product
Gerar Routes
luvit crescent-cli make:routes Product
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
- ✅ Controller
- ✅ Service
- ✅ Model
- ✅ Routes
- ✅ Module Init
src/product/
├── init.lua
├── controllers/
│ └── product.lua
├── services/
│ └── product.lua
├── models/
│ └── product.lua
└── routes/
└── product.lua
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
local ProductModule = require("src.product")
ProductModule.register(app)
🔄 Migrations
Criar Migration
luvit crescent-cli make:migration create_products_table
create_xxx_table→ Cria tabela "xxx"add_xxx_to_yyy→ Adiciona coluna na tabela "yyy"drop_xxx_table→ Remove tabela "xxx"update_xxx_table→ Atualiza tabela "xxx"
migrations/20260109123456_create_products_table.lua
Template gerado:
-- migrations/20260109123456_create_products_table.lua
local Migration = {}
function Migration:up()
return [[
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
]]
end
function Migration:down()
return [[
DROP TABLE IF EXISTS products;
]]
end
return Migration
Executar Migrations
# Executar todas pendentes
luvit crescent-cli migrate
# Desfazer última migration
luvit crescent-cli migrate:rollback
# Ver status
luvit crescent-cli migrate:status
🔄 Executando migrations pendentes...
✓ 20260108230701_create_users_table.lua aplicada
✓ 20260109123456_create_products_table.lua aplicada
✅ 2 migrations executadas com sucesso!
🎯 Casos de Uso Comuns
1. Criar CRUD Completo
# 1. Criar migration
luvit crescent-cli make:migration create_categories_table
# 2. Editar migration (adicionar campos)
nano migrations/20260109_*.lua
# 3. Executar migration
luvit crescent-cli migrate
# 4. Criar módulo completo
luvit crescent-cli make:module Category
# 5. Registrar no app.lua
# Adicionar: CategoryModule.register(app)
# 6. Iniciar servidor
luvit crescent-cli server
2. Adicionar Funcionalidade a Módulo Existente
# Adicionar novo controller
luvit crescent-cli make:controller Admin users
# Adicionar novo service
luvit crescent-cli make:service Auth auth
3. Workflow de Testes
# Criar teste
touch tests/test-product.lua
# Implementar teste usando crescent/utils/tests
nano tests/test-product.lua
# Rodar testes
luvit crescent-cli test
🔧 Instalação Global do CLI
Para usar crescent sem luvit crescent-cli:
# No diretório do framework
./install.sh
# Agora use diretamente
crescent make:module Product
crescent server
crescent test
💡 Dicas Avançadas
Aliases Bash
# Adicione no ~/.bashrc ou ~/.zshrc
alias cres='luvit crescent-cli'
alias cres-serve='luvit crescent-cli server'
alias cres-test='luvit crescent-cli test'
# Uso
cres make:module Product
cres-serve
cres-test
Watch Mode com Entr
# Auto-restart no servidor
find . -name "*.lua" | entr -r luvit crescent-cli server
# Auto-run tests
find tests -name "*.lua" | entr -c luvit crescent-cli test
Scripts Personalizados
Crie scripts no package.lua ou arquivos shell:
#!/bin/bash
# dev.sh - Script de desenvolvimento
echo "🔧 Instalando dependências..."
lit install
echo "🔄 Executando migrations..."
luvit crescent-cli migrate
echo "✅ Rodando testes..."
luvit crescent-cli test
echo "🚀 Iniciando servidor..."
luvit crescent-cli server
📖 Referência Rápida
# Criar projeto
crescent new app
# Geradores
crescent make:module User
crescent make:controller Product
crescent make:service Auth
crescent make:model Category
crescent make:routes Api
# Migrations
crescent make:migration create_posts_table
crescent migrate
crescent migrate:rollback
crescent migrate:status
# Desenvolvimento
crescent server
crescent test
# Help
crescent --help
🆘 Troubleshooting
Erro: "crescent-cli.lua not found"
Execute o comando no diretório raiz do projeto onde está o arquivo crescent-cli.lua.
Erro: "Permission denied"
chmod +x crescent-cli.lua
CLI não cria arquivos
Verifique permissões de escrita no diretório:
ls -la src/
Migration falha
- Verifique sintaxe SQL no arquivo da migration
- Confirme conexão com MySQL:
luvit crescent/database/mysql.lua - Veja logs de erro completos
📚 Próximas Seções
- Core Concepts - Rotas, Controllers, Services
- Database & ORM - Migrations e Models em detalhes
- Utilities - Ferramentas de teste e helpers
✅ Sistema de Testes
O Crescent inclui uma biblioteca completa de assertions para testes automatizados.
Criar Arquivo de Teste
-- tests/test-users.lua
local tests = require('crescent.utils.tests')
local userTests = {
testCreate = function()
-- Seu código de teste
tests.assertEquals(1 + 1, 2)
end,
testValidation = function()
tests.assertTrue(true)
tests.assertFalse(false)
end
}
tests.runSuite("User Tests", userTests)
Executar Testes
# Todos os testes
luvit crescent-cli test
# Teste específico
luvit tests/test-users.lua
📋 Assertions Disponíveis
Comparações Básicas
-- Igualdade
tests.assertEquals(actual, expected, "mensagem opcional")
tests.assertNotEquals(actual, expected)
-- Booleanos
tests.assertTrue(value)
tests.assertFalse(value)
Comparações Numéricas
tests.assertGreaterThan(5, 3) -- 5 > 3
tests.assertLessThan(3, 5) -- 3 < 5
tests.assertGreaterOrEqual(5, 5) -- 5 >= 5
tests.assertLessOrEqual(3, 5) -- 3 <= 5
tests.assertInRange(5, 1, 10) -- 1 <= 5 <= 10
Validações de Tipo
tests.assertType(value, "string")
tests.assertNil(value)
tests.assertNotNil(value)
tests.assertIsTable(value)
tests.assertIsFunction(value)
tests.assertIsString(value)
tests.assertIsNumber(value)
tests.assertIsBoolean(value)
Comparações de Strings
tests.assertContains("hello world", "world")
tests.assertNotContains("hello", "bye")
tests.assertStartsWith("hello world", "hello")
tests.assertEndsWith("hello world", "world")
tests.assertMatches("test123", "%d+") -- Lua pattern
Comparações de Tables
-- Comparação profunda
tests.assertTableEquals({a=1, b=2}, {a=1, b=2})
-- Contém valor
tests.assertTableContains({1, 2, 3}, 2)
-- Vazio/não vazio
tests.assertEmpty({})
tests.assertNotEmpty({1})
-- Tamanho
tests.assertLength({1, 2, 3}, 3)
tests.assertArrayLength({1, 2, 3}, 3)
Exceptions/Errors
-- Espera erro
tests.assertError(function()
error("Ops!")
end, "Ops!")
-- Não deve dar erro
tests.assertNoError(function()
return 1 + 1
end)
HTTP/API Testing
-- Status code
tests.assertStatusCode(response, 200)
-- Headers
tests.assertHeader(response, "Content-Type", "application/json")
-- JSON response
tests.assertJsonEquals('{"name":"John"}', {name="John"})
tests.assertJsonContains('{"name":"John","age":30}', "name", "John")
Identidade
local obj = {}
tests.assertSame(obj, obj) -- Mesma referência
tests.assertNotSame({}, {}) -- Objetos diferentes
📝 Exemplo Completo de Teste
-- tests/test-product.lua
local tests = require('crescent.utils.tests')
local Product = require('src.products.models.product')
local productTests = {
testCreate = function()
local product = Product:create({
name = "Notebook",
price = 2500
})
tests.assertNotNil(product)
tests.assertIsTable(product)
tests.assertEquals(product.name, "Notebook")
tests.assertType(product.price, "number")
tests.assertGreaterThan(product.price, 0)
end,
testValidation = function()
tests.assertError(function()
Product:create({name = ""}) -- Nome vazio
end, "validation failed")
end,
testFind = function()
local product = Product:find(1)
if product then
tests.assertIsTable(product)
tests.assertNotNil(product.id)
tests.assertType(product.name, "string")
end
end,
testUpdate = function()
local product = Product:find(1)
if product then
local oldName = product.name
product:update({name = "Updated Name"})
tests.assertNotEquals(product.name, oldName)
tests.assertEquals(product.name, "Updated Name")
end
end,
testAll = function()
local products = Product:all()
tests.assertIsTable(products)
tests.assertGreaterOrEqual(#products, 0)
end
}
-- Executar suite
tests.runSuite("Product Tests", productTests)
=== Test Suite: Product Tests ===
Running test: testCreate
✅ testCreate passed
Running test: testValidation
✅ testValidation passed
Running test: testFind
✅ testFind passed
Running test: testUpdate
✅ testUpdate passed
Running test: testAll
✅ testAll passed
=== Results: 5/5 passed, 0 failed ===
🔐 Hash de Senhas (PBKDF2)
Utilitário seguro para hash de senhas com salt aleatório.
Características
- ✅ PBKDF2 com SHA-256
- ✅ Salt aleatório único (16 bytes)
- ✅ 10.000 iterações por padrão
- ✅ Timing-safe comparison
- ✅ Senhas iguais geram hashes diferentes
Criar Hash de Senha
local hash = require('crescent.utils.hash')
-- Registrar usuário
local senha = "minhaSenhaSegura123"
local senhaHash = hash.encrypt(senha)
-- Resultado: "10000$a1b2c3d4e5f6...$9c8d7e6f5a4b..."
-- Armazenar senhaHash no banco de dados
user:update({password = senhaHash})
Verificar Senha (Login)
local hash = require('crescent.utils.hash')
-- No login
local senhaDigitada = ctx.body.password
local senhaArmazenada = user.password -- Hash do banco
if hash.verify(senhaDigitada, senhaArmazenada) then
-- Login bem-sucedido
return ctx.json(200, {token = gerarToken(user)})
else
-- Senha incorreta
return ctx.json(401, {error = "Credenciais inválidas"})
end
Exemplo Completo (Registro + Login)
-- src/auth/services/auth.lua
local hash = require('crescent.utils.hash')
local User = require('src.users.models.user')
local AuthService = {}
function AuthService:register(data)
-- Valida dados
if not data.email or not data.password then
error("Email e senha são obrigatórios")
end
-- Cria hash da senha
local passwordHash = hash.encrypt(data.password)
-- Cria usuário
local user = User:create({
name = data.name,
email = data.email,
password = passwordHash -- Armazena hash, não senha
})
return user
end
function AuthService:login(email, password)
-- Busca usuário
local user = User:where({email = email}):first()
if not user then
return nil, "Usuário não encontrado"
end
-- Verifica senha
if not hash.verify(password, user.password) then
return nil, "Senha incorreta"
end
-- Remove senha do retorno
user.password = nil
return user
end
return AuthService
Configurar Iterações
-- Mais seguro (mais lento)
local hash50k = hash.encrypt(senha, 50000)
-- Padrão (balanceado)
local hashPadrao = hash.encrypt(senha) -- 10.000 iterações
Aliases Disponíveis
-- Estas são equivalentes:
hash.encrypt(senha) -- ✅ Recomendado
hash.encript(senha) -- Alias (typo comum)
hash.verify(senha, hash) -- ✅ Recomendado
hash.decrypt(senha, hash) -- Alias
hash.decript(senha, hash) -- Alias
Hashes Simples (Checksums)
-- SHA-256 (para checksums, não senhas!)
local checksum = hash.sha256("conteúdo do arquivo")
-- MD5 (legado, não usar para senhas)
local md5sum = hash.md5("dados")
⚠️ Importante: Nunca use SHA-256 ou MD5 direto para senhas! Sempre use
hash.encrypt() que implementa PBKDF2 com salt.
🌐 Variáveis de Ambiente
local env = require('crescent.utils.env')
-- Carregar .env
env.load('.env')
-- Obter valores
local dbHost = env.get('DB_HOST', 'localhost') -- Com fallback
local port = env.get('PORT')
local isDev = env.get('ENV') == 'development'
-- Verificar se existe
if env.has('API_KEY') then
-- usa API_KEY
end
📨 Headers HTTP
local headers = require('crescent.utils.headers')
-- Parse header string
local contentType = headers.parse('Content-Type: application/json')
-- Normalize header name
local normalized = headers.normalize('content-type') -- "Content-Type"
-- Get header value
local value = headers.get(ctx.headers, 'authorization')
🛤️ Path Utilities
local pathUtil = require('crescent.utils.path')
-- Join paths
local fullPath = pathUtil.join('src', 'users', 'models', 'user.lua')
-- "src/users/models/user.lua"
-- Normalize path
local normalized = pathUtil.normalize('src//users/../models/./user.lua')
-- "src/models/user.lua"
-- Get directory
local dir = pathUtil.dirname('src/users/models/user.lua')
-- "src/users/models"
-- Get filename
local file = pathUtil.basename('src/users/models/user.lua')
-- "user.lua"
-- Get extension
local ext = pathUtil.extname('user.lua')
-- ".lua"
🔤 String Utilities
local stringUtil = require('crescent.utils.string')
-- Trim whitespace
local trimmed = stringUtil.trim(' hello ') -- "hello"
-- Split string
local parts = stringUtil.split('a,b,c', ',') -- {"a", "b", "c"}
-- Starts/ends with
local starts = stringUtil.startsWith('hello', 'hel') -- true
local ends = stringUtil.endsWith('hello', 'lo') -- true
-- Case transformations
local upper = stringUtil.upper('hello') -- "HELLO"
local lower = stringUtil.lower('HELLO') -- "hello"
local title = stringUtil.titleCase('hello world') -- "Hello World"
-- Slugify
local slug = stringUtil.slugify('Hello World!') -- "hello-world"
🧪 Testando Utilities
-- tests/test-utilities.lua
local tests = require('crescent.utils.tests')
local hash = require('crescent.utils.hash')
local stringUtil = require('crescent.utils.string')
local utilTests = {
testHashUnique = function()
local senha = "test123"
local hash1 = hash.encrypt(senha)
local hash2 = hash.encrypt(senha)
-- Hashes devem ser diferentes (salt único)
tests.assertNotEquals(hash1, hash2)
-- Mas ambos devem verificar corretamente
tests.assertTrue(hash.verify(senha, hash1))
tests.assertTrue(hash.verify(senha, hash2))
end,
testHashVerification = function()
local senha = "myPassword123"
local hashed = hash.encrypt(senha)
tests.assertTrue(hash.verify(senha, hashed))
tests.assertFalse(hash.verify("wrongPassword", hashed))
end,
testStringTrim = function()
tests.assertEquals(stringUtil.trim(" hello "), "hello")
tests.assertEquals(stringUtil.trim("hello"), "hello")
end,
testStringSplit = function()
local parts = stringUtil.split("a,b,c", ",")
tests.assertArrayLength(parts, 3)
tests.assertEquals(parts[1], "a")
tests.assertEquals(parts[3], "c")
end
}
tests.runSuite("Utility Tests", utilTests)
💡 Boas Práticas
Testes
- Organize por módulo:
tests/test-{module}.lua - Nomenclatura clara:
testCreate,testValidation - Um assert por conceito: Testes pequenos e focados
- Use mensagens descritivas: Facilita debug
- Rode antes de commit:
git pre-commit hook
Hash de Senhas
- Nunca armazene senhas em texto plano
- Use
hash.encrypt()sempre: PBKDF2 + salt - Não use SHA-256/MD5 para senhas
- Valide força da senha antes: regex, tamanho mínimo
- Considere 2FA para produção
Variáveis de Ambiente
- Nunca commite
.env: Use.env.example - Use fallbacks:
env.get('PORT', 8080) - Valide valores críticos: MySQL, API keys
- Diferentes arquivos por ambiente:
.env.dev,.env.prod
📖 Próximas Seções
- Database & ORM - Models, relações e migrations
- Core Concepts - Rotas, controllers e services
- Deployment - Deploy em produção
📋 Pré-requisitos
Servidor Linux
# Ubuntu/Debian
sudo apt update
sudo apt install -y curl git build-essential libssl-dev
# CentOS/RHEL
sudo yum install -y curl git gcc make openssl-devel
Instalar Luvit
curl -L https://github.com/luvit/lit/raw/master/get-lit.sh | sh
Adicionar ao PATH:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Instalar NGINX
# Ubuntu/Debian
sudo apt install -y nginx
# CentOS/RHEL
sudo yum install -y nginx
# Iniciar e habilitar
sudo systemctl start nginx
sudo systemctl enable nginx
Instalar MySQL/MariaDB
# Ubuntu/Debian
sudo apt install -y mysql-server
# CentOS/RHEL
sudo yum install -y mariadb-server
# Iniciar
sudo systemctl start mysql
sudo systemctl enable mysql
# Secure installation
sudo mysql_secure_installation
🔧 Configuração do Projeto
1. Clonar Projeto
cd /var/www
sudo git clone https://github.com/seu-usuario/seu-projeto.git meu-app
sudo chown -R $USER:$USER /var/www/meu-app
cd /var/www/meu-app
2. Instalar Dependências
# Crescent Framework
lit install daniel-m-tfs/crescent-framework
# Outras dependências (se houver)
lit install luvit/secure-socket
lit install luvit/json
3. Configurar Ambiente
cp .env.example .env
nano .env
# .env (produção)
APP_ENV=production
APP_PORT=8080
APP_HOST=127.0.0.1
DB_HOST=localhost
DB_PORT=3306
DB_NAME=meu_banco_producao
DB_USER=meu_usuario
DB_PASSWORD=senha_forte_aqui
JWT_SECRET=chave_super_secreta_aleatoria_64_caracteres_ou_mais
4. Criar Banco de Dados
mysql -u root -p
CREATE DATABASE meu_banco_producao CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'meu_usuario'@'localhost' IDENTIFIED BY 'senha_forte_aqui';
GRANT ALL PRIVILEGES ON meu_banco_producao.* TO 'meu_usuario'@'localhost';
FLUSH PRIVILEGES;
EXIT;
5. Executar Migrations
luvit crescent-cli migrate
🌐 NGINX Reverse Proxy
Configuração Básica
# /etc/nginx/sites-available/meu-app
server {
listen 80;
server_name meuapp.com www.meuapp.com;
# Logs
access_log /var/log/nginx/meu-app-access.log;
error_log /var/log/nginx/meu-app-error.log;
# Proxy para Luvit
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# Headers importantes
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Cache bypass
proxy_cache_bypass $http_upgrade;
}
# Arquivos estáticos (se houver)
location /static {
alias /var/www/meu-app/public;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Habilitar Site
# Criar link simbólico
sudo ln -s /etc/nginx/sites-available/meu-app /etc/nginx/sites-enabled/
# Testar configuração
sudo nginx -t
# Recarregar NGINX
sudo systemctl reload nginx
🔒 SSL/HTTPS com Let's Encrypt
Instalar Certbot
# Ubuntu/Debian
sudo apt install -y certbot python3-certbot-nginx
# CentOS/RHEL
sudo yum install -y certbot python3-certbot-nginx
Obter Certificado
sudo certbot --nginx -d meuapp.com -d www.meuapp.com
Configuração NGINX com SSL
# /etc/nginx/sites-available/meu-app
server {
listen 80;
server_name meuapp.com www.meuapp.com;
# Redirecionar para HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name meuapp.com www.meuapp.com;
# SSL
ssl_certificate /etc/letsencrypt/live/meuapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/meuapp.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS (opcional mas recomendado)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Logs
access_log /var/log/nginx/meu-app-access.log;
error_log /var/log/nginx/meu-app-error.log;
# Proxy para Luvit
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_cache_bypass $http_upgrade;
}
# Arquivos estáticos
location /static {
alias /var/www/meu-app/public;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Renovação Automática
# Testar renovação
sudo certbot renew --dry-run
# Crontab para renovação automática
sudo crontab -e
Adicionar:
0 0 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
🔧 Systemd Service
Criar Service Unit
sudo nano /etc/systemd/system/meu-app.service
[Unit]
Description=Crescent Framework Application
After=network.target mysql.service
Wants=mysql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/meu-app
Environment="PATH=/home/www-data/.local/bin:/usr/local/bin:/usr/bin:/bin"
Environment="APP_ENV=production"
ExecStart=/home/www-data/.local/bin/luvit bootstrap.lua
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=meu-app
# Segurança
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/meu-app
[Install]
WantedBy=multi-user.target
Gerenciar Service
# Recarregar systemd
sudo systemctl daemon-reload
# Habilitar (iniciar no boot)
sudo systemctl enable meu-app
# Iniciar
sudo systemctl start meu-app
# Status
sudo systemctl status meu-app
# Parar
sudo systemctl stop meu-app
# Reiniciar
sudo systemctl restart meu-app
# Logs em tempo real
sudo journalctl -u meu-app -f
📊 Monitoramento e Logs
Logs Systemd
# Últimas 100 linhas
sudo journalctl -u meu-app -n 100
# Seguir em tempo real
sudo journalctl -u meu-app -f
# Filtrar por data
sudo journalctl -u meu-app --since "2026-01-09"
# Filtrar por prioridade
sudo journalctl -u meu-app -p err
Logs NGINX
# Access log
sudo tail -f /var/log/nginx/meu-app-access.log
# Error log
sudo tail -f /var/log/nginx/meu-app-error.log
# Analisar códigos de status
awk '{print $9}' /var/log/nginx/meu-app-access.log | sort | uniq -c | sort -rn
# Top 10 IPs
awk '{print $1}' /var/log/nginx/meu-app-access.log | sort | uniq -c | sort -rn | head -10
Logs da Aplicação
-- Implementar logger
local Logger = {}
function Logger:log(level, message, data)
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
local logEntry = string.format(
"[%s] [%s] %s",
timestamp,
level,
message
)
if data then
logEntry = logEntry .. " " .. require('json').encode(data)
end
print(logEntry) -- systemd captura isso
end
function Logger:info(message, data)
self:log("INFO", message, data)
end
function Logger:error(message, data)
self:log("ERROR", message, data)
end
return Logger
⚡ Performance
NGINX Optimizations
# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
# Básico
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Buffer
client_body_buffer_size 128k;
client_max_body_size 10M;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
# Gzip
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss;
gzip_disable "msie6";
# Cache estático
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=auth:10m rate=3r/s;
}
MySQL Optimization
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# InnoDB
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
# Query cache
query_cache_size = 64M
query_cache_type = 1
# Connections
max_connections = 200
# Logs (desabilitar em produção para performance)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
Aplicar:
sudo systemctl restart mysql
🔐 Segurança
Firewall (UFW)
# Habilitar UFW
sudo ufw enable
# Permitir SSH
sudo ufw allow 22/tcp
# Permitir HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Verificar status
sudo ufw status
Fail2Ban
# Instalar
sudo apt install -y fail2ban
# Configurar
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
[nginx-http-auth]
enabled = true
[nginx-limit-req]
enabled = true
# Iniciar
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Status
sudo fail2ban-client status
Hardening MySQL
-- Remover usuários anônimos
DELETE FROM mysql.user WHERE User='';
-- Remover banco de teste
DROP DATABASE IF EXISTS test;
-- Permitir root apenas localmente
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
FLUSH PRIVILEGES;
Variáveis de Ambiente Seguras
# Nunca commite .env no Git!
# Usar variáveis do sistema
sudo nano /etc/systemd/system/meu-app.service
[Service]
Environment="DB_PASSWORD=senha_segura_aqui"
Environment="JWT_SECRET=token_secreto_aqui"
🔄 Deploy Automatizado
Script de Deploy
#!/bin/bash
# deploy.sh
set -e # Exit on error
APP_DIR="/var/www/meu-app"
BACKUP_DIR="/var/backups/meu-app"
echo "🚀 Starting deployment..."
# 1. Backup atual
echo "📦 Creating backup..."
mkdir -p $BACKUP_DIR
tar -czf $BACKUP_DIR/backup-$(date +%Y%m%d-%H%M%S).tar.gz $APP_DIR
# 2. Pull código
echo "📥 Pulling latest code..."
cd $APP_DIR
git pull origin main
# 3. Instalar dependências
echo "📚 Installing dependencies..."
lit install
# 4. Migrations
echo "🗄️ Running migrations..."
luvit crescent-cli migrate
# 5. Reiniciar serviço
echo "🔄 Restarting service..."
sudo systemctl restart meu-app
# 6. Verificar status
echo "✅ Checking status..."
sleep 2
sudo systemctl status meu-app --no-pager
# 7. Reload NGINX
echo "🌐 Reloading NGINX..."
sudo nginx -t && sudo systemctl reload nginx
echo "✨ Deployment complete!"
Tornar executável:
chmod +x deploy.sh
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/meu-app
./deploy.sh
🩺 Health Checks
Endpoint de Status
-- src/health/routes/health.lua
return function(router)
router:get("/health", function(ctx)
-- Verificar banco
local db = require('crescent.database.mysql')
local dbOk = pcall(function()
db:query("SELECT 1")
end)
return ctx.json(200, {
status = "ok",
timestamp = os.date("%Y-%m-%d %H:%M:%S"),
uptime = os.clock(),
database = dbOk and "connected" or "disconnected"
})
end)
end
Monitoramento Externo
# Criar script de verificação
nano /usr/local/bin/check-app.sh
#!/bin/bash
URL="https://meuapp.com/health"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $URL)
if [ $RESPONSE -eq 200 ]; then
echo "✅ App is healthy"
exit 0
else
echo "❌ App is down (HTTP $RESPONSE)"
# Reiniciar serviço
sudo systemctl restart meu-app
exit 1
fi
chmod +x /usr/local/bin/check-app.sh
# Cron a cada 5 minutos
crontab -e
*/5 * * * * /usr/local/bin/check-app.sh >> /var/log/app-health.log 2>&1
🐛 Troubleshooting
App não inicia
# Verificar logs
sudo journalctl -u meu-app -n 100
# Verificar sintaxe Lua
luvit -e "require('bootstrap')"
# Verificar permissões
ls -la /var/www/meu-app
sudo chown -R www-data:www-data /var/www/meu-app
Erro 502 Bad Gateway
# App está rodando?
sudo systemctl status meu-app
# Porta correta?
netstat -tulpn | grep 8080
# Testar localmente
curl http://localhost:8080
Banco de dados não conecta
# MySQL está rodando?
sudo systemctl status mysql
# Testar conexão
mysql -h localhost -u meu_usuario -p meu_banco_producao
# Verificar .env
cat .env | grep DB_
Logs grandes
# Limitar tamanho do journal
sudo journalctl --vacuum-time=7d
sudo journalctl --vacuum-size=500M
# Rotação de logs NGINX
sudo nano /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
systemctl reload nginx > /dev/null
endscript
}
📖 Próximas Seções
- Getting Started - Instalação e início
- Core Concepts - Routes, Controllers, Services
- Database - ORM e Migrations
- CLI - Comandos disponíveis