Chegamos ao ponto em que o app deixa de ser "uns arquivos numa pasta" e vira um pacote R. Essa é a mudança mental mais importante para produção: um pacote tem estrutura padronizada, gerencia dependências explicitamente, permite testes automatizados, gera documentação e é instalável em qualquer máquina de forma reprodutível. O golem é o framework que faz isso para Shiny — pegando os módulos de programação em Shiny e organizando-os numa arquitetura de produção. Este é o alicerce dos módulos de deploy (Docker, ShinyProxy) que vêm a seguir.
Estruturar uma aplicação Shiny como um pacote R com golem: entender a arquitetura de arquivos, o ciclo de desenvolvimento (dev/ scripts), a gestão de dependências, funções utilitárias, testes, e a preparação para deploy (run_app, app_sys, configuração por ambiente).
Razões para empacotar o app
Um app-script (app.R + arquivos soltos) funciona no seu computador. Um app-pacote funciona em qualquer lugar, do mesmo jeito. As vantagens concretas:
- Dependências explícitas: o
DESCRIPTIONlista tudo que o app precisa; instalar o pacote instala as dependências. Fim do "faltou instalar o pacote X no servidor". - Testes: com
testthat, você testa funções e módulos automaticamente — impossível de fazer bem num script monolítico. - Documentação: cada função tem sua página de ajuda (
roxygen2). - Namespace e organização: funções em
R/, dados emdata/, recursos eminst/app/www/, tudo com convenções que qualquer dev R reconhece. - Instalável e versionável:
R CMD buildgera um.tar.gz; o app tem versão, changelog, e instala com uma linha.
Golem não inventa uma estrutura nova — ele usa a estrutura de pacote R padrão e adiciona as peças específicas de Shiny.
Criando o projeto e a estrutura
# install.packages("golem")
golem::create_golem("meuDashboard")
Isso gera um pacote com a estrutura característica:
meuDashboard/
├── DESCRIPTION # metadados e DEPENDÊNCIAS do pacote
├── NAMESPACE # o que o pacote exporta/importa
├── R/
│ ├── app_ui.R # a UI principal (monta os módulos)
│ ├── app_server.R # o server principal (chama os módulos)
│ ├── app_config.R # configuração (lê inst/golem-config.yml)
│ ├── run_app.R # a função que INICIA o app
│ └── mod_*.R # seus módulos, um arquivo por módulo
├── inst/
│ ├── app/www/ # CSS, JS, imagens (a pasta www/)
│ └── golem-config.yml # config por ambiente (dev/prod)
├── dev/
│ ├── 01_start.R # script do setup inicial
│ ├── 02_dev.R # adicionar módulos, deps, etc. no dia a dia
│ └── 03_deploy.R # scripts de deploy
└── tests/ # testes automatizados (testthat)
A lógica: R/ guarda o código (UI, server, módulos, utilitários); inst/ guarda o que é servido/lido em runtime (recursos web, config); dev/ guarda scripts que você roda durante o desenvolvimento (não vão para produção).
O ciclo de desenvolvimento com os scripts dev/
O jeito golem de trabalhar é rodar funções dos scripts em dev/ em vez de criar arquivos na mão. Isso garante que tudo siga a convenção.
# --- dev/02_dev.R: o script que você mais usa ---
# Adicionar um módulo novo (cria R/mod_grafico.R com UI+server prontos)
golem::add_module(name = "grafico")
# Adicionar um arquivo de funções utilitárias
golem::add_utils("helpers") # cria R/utils_helpers.R
golem::add_fct("calculos") # cria R/fct_calculos.R
# Declarar uma dependência (adiciona ao DESCRIPTION corretamente)
usethis::use_package("dplyr")
usethis::use_package("leaflet")
# Adicionar um recurso web (CSS/JS em inst/app/www)
golem::add_css_file("custom")
golem::add_js_file("handlers")
golem::add_module("grafico") gera um R/mod_grafico.R já com o esqueleto mod_grafico_ui(id) + mod_grafico_server(id) — os módulos de programação em Shiny, agora com nomes e local padronizados. Você preenche a lógica; o Golem cuidou da estrutura.
Como UI e server principais amarram os módulos
O app_ui.R e o app_server.R são os "maestros": eles apenas compõem os módulos. Toda a lógica real vive nos mod_*.R.
# R/app_ui.R
app_ui <- function(request) {
tagList(
golem_add_external_resources(), # injeta CSS/JS de inst/app/www
bslib::page_sidebar(
title = "Meu Dashboard",
theme = bslib::bs_theme(brand = app_sys("app/www/_brand.yml")),
sidebar = bslib::sidebar(mod_filtros_ui("filtros")), # módulo de filtros
mod_grafico_ui("grafico"), # módulo de gráfico
mod_tabela_ui("tabela") # módulo de tabela
)
)
}
# R/app_server.R
app_server <- function(input, output, session) {
# Amarra os módulos: saída de um vira entrada do outro
dados_filt <- mod_filtros_server("filtros", dados = carregar_dados())
mod_grafico_server("grafico", dados = dados_filt)
mod_tabela_server("tabela", dados = dados_filt)
}
Repare como o padrão de comunicação de programação em Shiny (entrada por argumento, saída por retorno) se torna a arquitetura oficial do app: o app_server é literalmente o lugar onde você conecta os reativos entre módulos. Um app golem bem feito tem um app_server curto e legível — quase um índice do que o app faz.
Recursos e caminhos: app_sys e golem_add_external_resources
Num pacote, você não pode usar caminhos relativos soltos ("www/logo.png") — precisa de caminhos que funcionem depois de instalado. O app_sys() resolve o caminho para dentro de inst/.
# ERRADO num pacote — caminho relativo quebra após instalação
readLines("inst/app/www/config.txt")
# CERTO — app_sys() encontra o arquivo esteja o pacote onde estiver
readLines(app_sys("app/www/config.txt"))
# golem_add_external_resources() (em app_ui) registra a pasta www e injeta
# automaticamente os CSS/JS de inst/app/www no <head>
Essa é a razão de inst/app/www existir: é o www/ de arquitetura responsiva, movido para o lugar padronizado de um pacote, acessível via app_sys("app/www/...") mesmo depois de o pacote ser instalado num servidor.
Configuração por ambiente: golem-config.yml
Apps de produção precisam de configurações diferentes em desenvolvimento e produção (URL do banco, nível de log, chaves). O inst/golem-config.yml centraliza isso.
# inst/golem-config.yml
default:
app_title: "Meu Dashboard"
data_path: "data/dev.csv"
log_level: "DEBUG"
production:
data_path: "/mnt/dados/prod.csv"
log_level: "ERROR"
# No código, lê-se a config do ambiente ativo:
get_golem_config("data_path") # retorna o valor do ambiente atual
# O ambiente é definido por variável de ambiente:
# GOLEM_CONFIG_ACTIVE=production → usa o bloco 'production'
Combine isso com a lição de segredos de notificações, validação e segurança: valores não sensíveis (títulos, caminhos, flags) vão no golem-config.yml; segredos (senhas, tokens) continuam em variáveis de ambiente (Sys.getenv), nunca no YAML versionado.
run_app: o ponto de entrada
O app inteiro é iniciado por uma única função exportada, run_app(). É ela que o servidor de produção chama.
# R/run_app.R
run_app <- function(onStart = NULL, options = list(), ...) {
with_golem_options(
app = shinyApp(ui = app_ui, server = app_server, options = options),
golem_opts = list(...)
)
}
Depois de instalar o pacote, iniciar o app em qualquer máquina é:
library(meuDashboard)
run_app()
# ou passar opções: run_app(options = list(port = 3838))
Essa é a beleza do modelo de pacote: o deploy (Docker, ShinyProxy) só precisa instalar o pacote e chamar meuDashboard::run_app() — sem se preocupar com onde estão os arquivos, porque tudo está empacotado.
Testes e verificação
Como é um pacote, você ganha o ferramental de qualidade do R de graça:
# Adicionar um teste para uma função utilitária
usethis::use_test("calculos")
# tests/testthat/test-calculos.R
test_that("calcula margem corretamente", {
expect_equal(calcular_margem(receita = 100, custo = 60), 0.4)
})
# Rodar todos os testes
devtools::test()
# Verificação completa do pacote (o mesmo do CRAN): estrutura, deps, docs
devtools::check()
Módulos também são testáveis com testServer(), que exercita a lógica reativa de um módulo sem subir a UI:
test_that("módulo filtra por região", {
testServer(mod_filtros_server, args = list(dados = dados_teste), {
session$setInputs(regiao = "Sul")
expect_true(all(output_reativo()$regiao == "Sul"))
})
})
Testabilidade é a recompensa concreta de empacotar: no script monolítico, testar a lógica reativa isolada era praticamente impossível; num módulo golem, é uma função de teste.
O Golem transforma o app num pacote R, o que traz dependências explícitas no DESCRIPTION, testes, documentação e instalação reprodutível. A estrutura tem três pastas com papéis claros: R/ para o código — app_ui, app_server, os mod_* e utilitários —, inst/ para recursos web e configuração em runtime, e dev/ para os scripts de desenvolvimento. O trabalho se faz rodando funções desses scripts: add_module, add_utils ou add_fct, use_package, add_css_file. O app_ui e o app_server apenas compõem módulos, e o padrão de comunicação — entrada por argumento, saída por retorno — deixa de ser convenção e vira a arquitetura oficial. O app_sys("app/www/...") resolve caminhos dentro do pacote instalado, e o inst/app/www é o www/ padronizado. A configuração por ambiente mora no golem-config.yml, com blocos default e production selecionados por GOLEM_CONFIG_ACTIVE; segredos continuam em variáveis de ambiente. O run_app() vira o ponto de entrada único, de modo que o deploy só precisa instalar o pacote e chamá-lo. E empacotar habilita o que antes era difícil: testthat, devtools::check() e o testServer() para testar a lógica reativa dos módulos.
Exercícios
-
Cite três vantagens concretas de estruturar um app Shiny como pacote (golem) em vez de um
app.Rmonolítico.✓ Resposta:(1) Dependências explícitas — o
DESCRIPTIONlista todos os pacotes necessários, então instalar o app instala suas dependências, eliminando o "faltou instalar X no servidor". (2) Testes automatizados — comtestthat/testServeré possível testar funções e módulos isoladamente, o que é impraticável num script gigante. (3) Reprodutibilidade e deploy — o app vira um pacote instalável e versionado, iniciado porrun_app()em qualquer máquina do mesmo jeito, o que simplifica Docker/ShinyProxy. (Também valem: documentação por função, organização padronizada, namespace.) -
Qual a diferença de propósito entre as pastas
R/,inst/edev/num projeto golem?✓ Resposta:R/guarda o código do app que roda em produção —app_ui,app_server, os módulosmod_*e funções utilitárias.inst/guarda o que é lido/servido em runtime e é instalado junto com o pacote — recursos web (inst/app/www: CSS, JS, imagens) e a configuração (golem-config.yml).dev/guarda scripts de desenvolvimento que você roda para construir o app (adicionar módulos, deps, deploy) e que não fazem parte do app em produção. Em resumo:R/= app;inst/= recursos/config em runtime;dev/= ferramentas do desenvolvedor. -
Por que num pacote golem você usa
app_sys("app/www/logo.png")em vez de"inst/app/www/logo.png"?✓ Resposta:Porque depois que o pacote é instalado, a pasta
inst/deixa de existir com esse nome — seu conteúdo é copiado para o diretório de instalação do pacote, cujo caminho varia conforme a máquina/servidor. Um caminho relativo como"inst/app/www/logo.png"só funciona no diretório de desenvolvimento e quebra após a instalação.app_sys("app/www/logo.png")(baseado emsystem.file()) resolve o caminho correto onde quer que o pacote esteja instalado, garantindo que o recurso seja encontrado em dev e em produção. -
Como o golem trata configurações diferentes entre desenvolvimento e produção, e onde os segredos NÃO devem ficar?
✓ Resposta:Pelo arquivo
inst/golem-config.yml, que tem blocos por ambiente (default,production, ...). O ambiente ativo é escolhido pela variável de ambienteGOLEM_CONFIG_ACTIVE(ex.:=production), e no código lê-se os valores comget_golem_config("chave"). Valores não sensíveis (títulos, caminhos de dados, níveis de log, flags) vão nesse YAML. Segredos (senhas de banco, tokens, chaves de API) não devem ficar nogolem-config.yml, porque ele é versionado no Git; eles continuam em variáveis de ambiente lidas comSys.getenv(), como visto em notificações, validação e segurança. -
O que é
run_app()e por que ele simplifica o deploy?✓ Resposta:run_app()é a função de entrada única exportada pelo pacote, que constrói e inicia oshinyApp(ui = app_ui, server = app_server)(envolto emwith_golem_options). Ela simplifica o deploy porque o ambiente de produção não precisa saber nada sobre a estrutura interna do app nem sobre onde estão os arquivos: basta instalar o pacote e chamarmeuPacote::run_app(). Como todos os recursos estão empacotados eminst/e resolvidos porapp_sys(), o mesmo comando funciona idêntico em Docker, ShinyProxy ou Posit Connect — o app é autocontido e reprodutível.