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 DESCRIPTION lista 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 em data/, recursos em inst/app/www/, tudo com convenções que qualquer dev R reconhece.
  • Instalável e versionável: R CMD build gera 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

  1. Cite três vantagens concretas de estruturar um app Shiny como pacote (golem) em vez de um app.R monolítico.

    ✓ Resposta:

    (1) Dependências explícitas — o DESCRIPTION lista todos os pacotes necessários, então instalar o app instala suas dependências, eliminando o "faltou instalar X no servidor". (2) Testes automatizados — com testthat/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 por run_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.)

  2. Qual a diferença de propósito entre as pastas R/, inst/ e dev/ num projeto golem?

    ✓ Resposta:

    R/ guarda o código do app que roda em produção — app_ui, app_server, os módulos mod_* 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.

  3. 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 em system.file()) resolve o caminho correto onde quer que o pacote esteja instalado, garantindo que o recurso seja encontrado em dev e em produção.

  4. 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 ambiente GOLEM_CONFIG_ACTIVE (ex.: =production), e no código lê-se os valores com get_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 no golem-config.yml, porque ele é versionado no Git; eles continuam em variáveis de ambiente lidas com Sys.getenv(), como visto em notificações, validação e segurança.

  5. 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 o shinyApp(ui = app_ui, server = app_server) (envolto em with_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 chamar meuPacote::run_app(). Como todos os recursos estão empacotados em inst/ e resolvidos por app_sys(), o mesmo comando funciona idêntico em Docker, ShinyProxy ou Posit Connect — o app é autocontido e reprodutível.

Fontes e leituras recomendadas