Voltar ao blog
05/02/2026

Como desenvolvi minha primeira biblioteca open-source pra Elixir

Post extraído do TabNews

TabNews
Conteúdo extraído do TabNews

Esses dias eu cheguei em um nível onde simplesmente abandonei meu github pessoal, estava lá ele: alguns commits feitos em repositórios privados e meus projetos públicos totalmente abandonados e sem vida.

Vendo essa situação, comecei a procurar alguma ideia interessante pra criar com Elixir, uma linguagem incrível e que todo mundo deveria dar um pouco de atenção a ela. Com algumas pesquisas e alguns prompts jogados no Gemini, resolvi criar um wrapper do IBGE no elixir. Foi ai que achei a biblioteca brasilapi-ex, que ja abstrai uma parte da api do IBGE, porém voltada a abstrair outras API's para o cenário brasileiro.

Resolvi então começar o desenvolvimento, a primeira coisa: como padronizar meu projeto? O que é considerado boas práticas para criar bibliotecas em Elixir? A onde eu estou me metendo?

Primeiro passo: Library Guidelines. Ponto de partida pra criar uma biblioteca em Elixir, aqui contém uma série de instruções recomendadas pra manter uma boa padronização pra sua biblioteca. Algumas das instruções:

  • Todo projeto deve começar com snake_case.
  • Escrever testes com ExUnit(Framework de testes unitários do Elixir)
  • Escolher modo de versionamento(a maioria dos projetos utilizam SemVer)
  • Escrever documentação. A maioria dos projetos utilizam ExDoc pra isso.

Boas práticas e "Code Smells": Seguindo uma recomendação da própria documentação do Elixir, mergulhei no catálogo de Anti-Patterns criado por Lucas Vegi e Marco Tulio Valente. Esse material virou meu guia de bolso: sempre que eu sentia que o código estava ficando "estranho" ou complexo demais, eu corria para lá para garantir que não estava cometendo nenhum pecado capital da linguagem.

Comecei então o desenvolvimento da biblioteca, seguindo a seguinte arquitetura:

lib/ex_ibge/
├── aggregate.ex
├── api.ex
├── name.ex
├── query.ex
├── utils.ex
├── geography/
└── locality/ 

Configurei um client básico para padronizar as consultas e criei a função bangify/1 para utilizar em bang functions(funções que retornam o valor caso dê certo ou gera uma raise expection, matando o processo):

defmodule ExIbge.Api do
  @base_url "https://servicodados.ibge.gov.br/api"
  @versions [:v1, :v2, :v3, :v4]

  @doc """
  Create a new client for a specific version of the API.
  """
  @type t :: Req.Request.t()

  @spec new!(atom()) :: t()
  def new!(version) when version in @versions do
    options = Application.get_env(:ex_ibge, :req_options, [])
    Req.new([base_url: "#{@base_url}/#{version}"] ++ options)
  end

  def new!(version) do
    raise ArgumentError, message: "Invalid version: #{version}"
  end

  @doc """
  Bangify a result.
  """
  @spec bangify({:ok, any()} | {:error, any()} | :ok) :: any()
  def bangify({:error, error}), do: raise(error)
  def bangify({:ok, body}), do: body
  def bangify(:ok), do: :ok
end

Para a implementação dos endpoints, segui um padrão importante em bibliotecas Elixir: oferecer escolha. Criei a função all/1, que segue o "caminho feliz" seguro retornando uma tupla {:ok, result} ou {:error, reason}, ideal para sistemas robustos que precisam tratar falhas.

Mas também implementei a versão all!/1 (a famosa bang function). Ela é perfeita para scripts rápidos ou para quando você quer encadear funções no pipe operator (|>) e prefere que o processo quebre imediatamente caso algo dê errado, sem precisar ficar "desembrulhando" tuplas manualmente.

  @spec all(Keyword.t()) :: {:ok, list(Country.t())} | {:error, any()}
  def all(query \\ []) do
    Req.get(Api.new!(:v1),
      url: "/localidades/paises",
      params: Query.build(query, Geography.Country)
    )
    |> handle_response()
  end
  
  @spec all!(Keyword.t()) :: list(Country.t())
  def all!(query \\ []) do
    Api.bangify(all(query))
  end

o Elixir realmente brilha é no tratamento da resposta da API. Em vez de encher o código de if/else para verificar se o status é 200 ou se o corpo é uma lista, utilizei Pattern Matching na função privada handle_response.

O código fica extremamente declarativo:

  • Se for 200 e o corpo for uma lista, mapeio para structs.
  • Se for 200 mas o corpo for um mapa único, envolvo numa lista.
  • Qualquer outro status vira erro.
  • Erros de conexão (HTTP) são capturados na última cláusula.
  defp handle_response({:ok, %{status: 200, body: data}}) when is_list(data) do
    countries = Enum.map(data, &Country.from_map/1)
    {:ok, countries}
  end

  defp handle_response({:ok, %{status: 200, body: data}}) when is_map(data) do
    {:ok, [Country.from_map(data)]}
  end

  defp handle_response({:ok, %{status: status}}) do
    {:error, {:http_error, status}}
  end

  defp handle_response({:error, error}) do
    {:error, error}
  end

No fim das contas, mais do que entregar um wrapper para o IBGE, esse projeto serviu para tirar a poeira do meu GitHub e, finalmente, colocar em prática conceitos de arquitetura e boas práticas que eu só via na teoria.

A biblioteca ainda está em crescimento (afinal, o IBGE tem dados infinitos), mas a base está sólida, testada e seguindo os padrões da comunidade. Se você também está estudando Elixir, quer contribuir ou apenas ver meu código, o código completo está aqui: ExIbge