# Como Criar uma Gem Ruby para Encapsular Chamadas à API de CPF

> Aprenda a criar uma gem Ruby para encapsular chamadas à API de CPF, com configuração, tratamento de erros, testes e publicação no RubyGems.

**Publicado:** 23/08/2024
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/criar-gem-ruby-encapsular-chamadas-api-cpf

---


Criar uma gem Ruby para encapsular chamadas à API de CPF centraliza a lógica de integração em um único lugar, padroniza o tratamento de erros e permite que qualquer aplicação da organização consulte o endpoint `https://api.cpfhub.io/cpf/{CPF}` com uma única linha no Gemfile. O processo envolve configurar o cliente Faraday, definir a hierarquia de exceções e cobrir os cenários com RSpec e WebMock.

## Introdução

Quando múltiplas aplicações Ruby da sua organização consomem a mesma API de CPF, encapsular as chamadas em uma gem centraliza a lógica de integração, padroniza o tratamento de erros e facilita a manutenção. Uma gem bem construída permite que qualquer desenvolvedor integre a API com uma única linha no Gemfile.

---

## Estrutura da gem

Crie a estrutura básica da gem usando o Bundler.

```ruby
# Estrutura de diretórios
# cpfhub/
# lib/
# cpfhub/
# client.rb
# configuration.rb
# errors.rb
# response.rb
# version.rb
# cpfhub.rb
# spec/
# cpfhub/
# client_spec.rb
# spec_helper.rb
# cpfhub.gemspec
# Gemfile
# README.md

# cpfhub.gemspec
Gem::Specification.new do |spec|
 spec.name = "cpfhub"
 spec.version = CpfHub::VERSION
 spec.authors = ["Seu Nome"]
 spec.email = ["seu@email.com"]
 spec.summary = "Cliente Ruby para a API do CPFHub"
 spec.description = "Gem para consultar e validar CPFs via API do CPFHub"
 spec.homepage = "https://github.com/seu-usuario/cpfhub-ruby"
 spec.license = "MIT"
 spec.required_ruby_version = ">= 3.0"

 spec.add_dependency "faraday", "~> 2.0"
 spec.add_dependency "faraday-retry", "~> 2.0"

 spec.add_development_dependency "rspec", "~> 3.12"
 spec.add_development_dependency "webmock", "~> 3.18"
end
```

| Arquivo | Responsabilidade |
|---|---|
| `lib/cpfhub.rb` | Ponto de entrada, configuração global |
| `lib/cpfhub/client.rb` | Cliente HTTP principal |
| `lib/cpfhub/configuration.rb` | Classe de configuração |
| `lib/cpfhub/errors.rb` | Hierarquia de exceções |
| `lib/cpfhub/response.rb` | Wrapper para respostas da API |
| `lib/cpfhub/version.rb` | Versão da gem |

---

## Módulo principal e configuração

O módulo principal expõe a configuração global e um método de conveniência para consultas.

```ruby
# lib/cpfhub/version.rb
module CpfHub
 VERSION = "1.0.0"
end

# lib/cpfhub/configuration.rb
module CpfHub
 class Configuration
 attr_accessor :api_key, :base_url, :timeout, :open_timeout,
 :max_retries, :retry_delay, :logger

 def initialize
 @api_key = ENV["CPFHUB_API_KEY"]
 @base_url = "https://api.cpfhub.io"
 @timeout = 10
 @open_timeout = 5
 @max_retries = 3
 @retry_delay = 1
 @logger = Logger.new($stdout)
 end
 end
end

# lib/cpfhub.rb
require "cpfhub/version"
require "cpfhub/configuration"
require "cpfhub/errors"
require "cpfhub/response"
require "cpfhub/client"

module CpfHub
 class << self
 attr_accessor :configuration

 def configure
 self.configuration ||= Configuration.new
 yield(configuration)
 end

 def client
 @client ||= Client.new
 end

 def consultar(cpf)
 client.consultar(cpf)
 end

 def reset!
 @client = nil
 @configuration = nil
 end
 end
end
```

---

## Cliente HTTP e tratamento de erros

O cliente encapsula toda a comunicação com a API.

```ruby
# lib/cpfhub/errors.rb
module CpfHub
 class Error < StandardError; end
 class ConfigurationError < Error; end
 class InvalidCpfError < Error; end
 class AuthenticationError < Error; end
 class NotFoundError < Error; end
 class RateLimitError < Error; end
 class ServerError < Error; end
 class TimeoutError < Error; end
end

# lib/cpfhub/response.rb
module CpfHub
 class Response
 attr_reader :cpf, :name, :name_upper, :gender,
 :birth_date, :day, :month, :year, :raw

 def initialize(data)
 @raw = data
 @cpf = data["cpf"]
 @name = data["name"]
 @name_upper = data["nameUpper"]
 @gender = data["gender"]
 @birth_date = data["birthDate"]
 @day = data["day"]
 @month = data["month"]
 @year = data["year"]
 end

 def masculino?
 gender == "M"
 end

 def feminino?
 gender == "F"
 end

 def to_h
 raw
 end
 end
end

# lib/cpfhub/client.rb
require "faraday"
require "faraday-retry"
require "json"

module CpfHub
 class Client
 def initialize(config = nil)
 @config = config || CpfHub.configuration || Configuration.new
 validate_config!
 @connection = build_connection
 end

 def consultar(cpf)
 cpf_limpo = sanitizar_cpf(cpf)
 validar_cpf!(cpf_limpo)

 resposta = @connection.get("/cpf/#{cpf_limpo}")
 tratar_resposta(resposta)
 rescue Faraday::TimeoutError
 raise CpfHub::TimeoutError, "Timeout ao consultar CPF"
 rescue Faraday::ConnectionFailed => e
 raise CpfHub::Error, "Falha de conexao: #{e.message}"
 end

 private

 def validate_config!
 if @config.api_key.nil? || @config.api_key.empty?
 raise ConfigurationError,
 "API key nao configurada. Use CpfHub.configure"
 end
 end

 def build_connection
 Faraday.new(url: @config.base_url) do |conn|
 conn.request :retry, max: @config.max_retries,
 interval: @config.retry_delay,
 backoff_factor: 2,
 exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
 conn.headers["x-api-key"] = @config.api_key
 conn.headers["Content-Type"] = "application/json"
 conn.headers["User-Agent"] = "cpfhub-ruby/#{VERSION}"
 conn.options.timeout = @config.timeout
 conn.options.open_timeout = @config.open_timeout
 conn.adapter Faraday.default_adapter
 end
 end

 def sanitizar_cpf(cpf)
 cpf.to_s.gsub(/\D/, "")
 end

 def validar_cpf!(cpf)
 unless cpf.match?(/\A\d{11}\z/)
 raise InvalidCpfError, "CPF deve conter 11 digitos"
 end
 end

 def tratar_resposta(resposta)
 case resposta.status
 when 200
 body = JSON.parse(resposta.body)
 if body["success"]
 Response.new(body["data"])
 else
 raise NotFoundError, "CPF nao encontrado"
 end
 when 401
 raise AuthenticationError, "API key invalida"
 when 429
 raise RateLimitError, "Rate limit excedido"
 when 500..599
 raise ServerError, "Erro no servidor (#{resposta.status})"
 else
 raise Error, "Resposta inesperada (#{resposta.status})"
 end
 end
 end
end
```

---

## Testes com RSpec e WebMock

Testes garantem que a gem funciona corretamente em todos os cenários.

```ruby
# spec/spec_helper.rb
require "webmock/rspec"
require "cpfhub"

RSpec.configure do |config|
 config.before(:each) do
 CpfHub.configure do |c|
 c.api_key = "test-api-key"
 end
 end

 config.after(:each) do
 CpfHub.reset!
 end
end

# spec/cpfhub/client_spec.rb
RSpec.describe CpfHub::Client do
 let(:client) { described_class.new }
 let(:cpf) { "12345678901" }

 describe "#consultar" do
 context "quando o CPF e encontrado" do
 before do
 stub_request(:get, "https://api.cpfhub.io/cpf/#{cpf}")
 .with(headers: { "x-api-key" => "test-api-key" })
 .to_return(
 status: 200,
 body: {
 success: true,
 data: {
 cpf: cpf, name: "Joao da Silva",
 nameUpper: "JOAO DA SILVA", gender: "M",
 birthDate: "1990-01-15", day: 15, month: 1, year: 1990
 }
 }.to_json
 )
 end

 it "retorna um Response com os dados" do
 resultado = client.consultar(cpf)
 expect(resultado).to be_a(CpfHub::Response)
 expect(resultado.name).to eq("Joao da Silva")
 expect(resultado.gender).to eq("M")
 expect(resultado.masculino?).to be true
 end
 end

 context "quando o CPF e invalido" do
 it "levanta InvalidCpfError" do
 expect { client.consultar("123") }
 .to raise_error(CpfHub::InvalidCpfError)
 end
 end

 context "quando a API retorna 429" do
 before do
 stub_request(:get, "https://api.cpfhub.io/cpf/#{cpf}")
 .to_return(status: 429)
 end

 it "levanta RateLimitError" do
 expect { client.consultar(cpf) }
 .to raise_error(CpfHub::RateLimitError)
 end
 end
 end
end
```

---

## Uso da gem

Com a gem publicada, a integração em qualquer aplicação Ruby é simples.

```ruby
# Gemfile
gem "cpfhub"

# config/initializers/cpfhub.rb (Rails)
CpfHub.configure do |config|
 config.api_key = ENV["CPFHUB_API_KEY"]
 config.timeout = 15
 config.max_retries = 3
end

# Uso em qualquer parte da aplicação
resultado = CpfHub.consultar("123.456.789-01")
puts resultado.name # => "Joao da Silva"
puts resultado.gender # => "M"
puts resultado.birth_date # => "1990-01-15"
puts resultado.masculino? # => true
```

---

## Perguntas frequentes

### Como a gem lida com o excedente de consultas no plano gratuito da CPFHub.io?
A API CPFHub.io não bloqueia requisições ao atingir o limite de 50 consultas mensais do plano gratuito — ela cobra R$0,15 por consulta adicional. A gem não precisa de nenhuma lógica especial para isso, mas é boa prática monitorar o consumo pelo dashboard e implementar um contador local para evitar cobranças inesperadas.

### É possível usar a gem em projetos Rails com múltiplos workers em paralelo?
Sim. A gem é thread-safe desde que cada instância de `CpfHub::Client` seja criada com sua própria configuração, ou que a configuração global seja definida em um initializer antes do fork dos workers. Use `CpfHub.configure` no `config/initializers/cpfhub.rb` para garantir que a `api_key` seja carregada antes do Puma ou Sidekiq iniciarem.

### Como testar a gem sem consumir consultas reais da API?
Use WebMock para interceptar as requisições HTTP nos testes. O exemplo com `stub_request` mostrado neste artigo permite simular respostas de sucesso, CPF não encontrado, timeout e rate limit sem fazer nenhuma chamada real à API CPFHub.io. Para testes de integração pontuais, o [OWASP Testing Guide](https://owasp.org/www-project-web-security-testing-guide/) recomenda ambientes de staging isolados.

### Como publicar a gem no RubyGems após o desenvolvimento?
Execute `gem build cpfhub.gemspec` para gerar o arquivo `.gem`, depois `gem push cpfhub-1.0.0.gem` com sua conta autenticada no RubyGems. Lembre-se de incrementar a versão em `lib/cpfhub/version.rb` a cada release e manter um `CHANGELOG` documentando as mudanças.

### Leia também

- [Como validar CPF no frontend com React e API REST](https://cpfhub.io/blog/como-validar-cpf-no-frontend-com-react-e-api-rest)
- [SLA de API de CPF: níveis de disponibilidade e o que exigir do seu provedor](https://cpfhub.io/blog/sla-api-cpf-niveis-disponibilidade)
- [API de CPF grátis para desenvolvedores: como começar em 5 minutos](https://cpfhub.io/blog/api-cpf-gratis-desenvolvedores-comecar-5-minutos)
- [10 erros mais comuns ao integrar uma API de CPF e como evitá-los](https://cpfhub.io/blog/10-erros-mais-comuns-ao-integrar-uma-api-de-cpf)

---

## Conclusão

Criar uma gem Ruby para encapsular chamadas à API de CPF centraliza a lógica de integração, padroniza o tratamento de erros e simplifica a manutenção. Com configuração flexível, tratamento robusto de exceções e testes abrangentes, a gem permite que qualquer aplicação Ruby da organização integre a API de CPF com uma única dependência.

Cadastre-se em [cpfhub.io](https://www.cpfhub.io/) — 50 consultas mensais gratuitas, sem cartão de crédito — e comece a construir sua gem Ruby com acesso completo ao endpoint de consulta de CPF da CPFHub.io.

