Skip to content

Instantly share code, notes, and snippets.

@devton
Last active January 20, 2026 15:43
Show Gist options
  • Select an option

  • Save devton/4d5889622107c68aa6e53223c34cc17a to your computer and use it in GitHub Desktop.

Select an option

Save devton/4d5889622107c68aa6e53223c34cc17a to your computer and use it in GitHub Desktop.
br banks holidays forfun

Mini API de Feriados Bancários no Brasil

Descrição

Esta é uma mini API em Go que fornece uma lista de feriados bancários nacionais no Brasil. A aplicação baixa os dados do site da FEBRABAN, os armazena em um banco de dados e os expõe através de um endpoint de API RESTful.

O projeto foi construído como um exercício para demonstrar a criação de uma API em Go, web scraping e interação com um banco de dados.

Funcionalidades

  • Scraping de Feriados: A aplicação faz o scraping dos feriados bancários do site da FEBRABAN para o ano corrente e o próximo.
  • Armazenamento em Banco de Dados: Os feriados são armazenados em um banco de dados SQLite local (holidays.db), que pode ser facilmente substituído por uma solução como o Turso.
  • API RESTful: Expõe um endpoint /holidays que retorna a lista de feriados em formato JSON.
  • Atualização Periódica: O scraping é executado na inicialização e, em seguida, a cada 24 horas para manter os dados atualizados.
  • Testes Automatizados: O projeto inclui testes de unidade para garantir a corretude do código.

Tecnologias Utilizadas

  • Linguagem: Go (versão 1.25.6)
  • Roteamento HTTP: Gorilla Mux
  • Banco de Dados: SQLite (usando o driver mattn/go-sqlite3), compatível com Turso
  • Testes: Pacotes nativos do Go (testing, net/http/httptest)

Endpoints da API

GET /holidays

Retorna uma lista de todos os feriados bancários armazenados no banco de dados.

Exemplo de Resposta:

[
    {
        "id": 1,
        "date": "2026-01-01",
        "description": "Confraternização Universal"
    },
    {
        "id": 2,
        "date": "2026-02-16",
        "description": "Carnaval"
    },
    {
        "id": 3,
        "date": "2026-02-17",
        "description": "Carnaval"
    }
]

Como Executar

  1. Pré-requisitos:

    • Ter o Go (versão 1.25.6 ou superior) instalado.
  2. Instalar as dependências: No terminal, na raiz do projeto, execute o seguinte comando para baixar as dependências:

    go mod tidy
  3. Executar a aplicação:

    go run main.go

    O servidor será iniciado em http://localhost:8080.

Como Executar os Testes

Para executar os testes automatizados, rode o seguinte comando no terminal:

go test -v
package main
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
_ "github.com/mattn/go-sqlite3" // SQLite driver for Turso
)
// Holiday represents a bank holiday
type Holiday struct {
ID int `json:"id"`
Date string `json:"date"`
Description string `json:"description"`
}
// FebrabanHoliday represents the structure of the holiday data from the FEBRABAN API
type FebrabanHoliday struct {
DiaMes string `json:"diaMes"`
DiaSemana string `json:"diaSemana"`
NomeFeriado string `json:"nomeFeriado"`
}
var db *sql.DB
const febrabanBaseURL = "https://feriadosbancarios.febraban.org.br/Home/"
func initializeDB() error {
var err error
db, err = sql.Open("sqlite3", "./holidays.db")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
createTableSQL := `CREATE TABLE IF NOT EXISTS holidays (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL UNIQUE,
description TEXT NOT NULL
);`
_, err = db.Exec(createTableSQL)
if err != nil {
return fmt.Errorf("failed to create holidays table: %w", err)
}
log.Println("Database initialized successfully.")
return nil
}
var doHTTPRequest = func(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request to %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("received non-OK HTTP status for %s: %s", url, resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body from %s: %w", url, err)
}
return body, nil
}
var timeNow = time.Now
func scrapeHolidays() {
log.Println("Starting to scrape holidays...")
currentYear := timeNow().Year()
yearsToScrape := []int{currentYear, currentYear + 1}
for _, year := range yearsToScrape {
log.Printf("Scraping holidays for year %d...", year)
endpoints := []string{"ObterFeriadosFederais", "ObterFeriadosFederaisF"}
for _, endpoint := range endpoints {
apiURL := fmt.Sprintf("%s%s", febrabanBaseURL, endpoint)
params := url.Values{}
params.Add("ano", strconv.Itoa(year))
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
body, err := doHTTPRequest(fullURL)
if err != nil {
log.Printf("Error fetching holidays from %s for year %d: %v", endpoint, year, err)
continue
}
var febrabanHolidays []FebrabanHoliday
if err := json.Unmarshal(body, &febrabanHolidays); err != nil {
log.Printf("Error unmarshaling JSON from %s for year %d: %v", endpoint, year, err)
continue
}
for _, fh := range febrabanHolidays {
fullDate, err := parseFebrabanDate(fh.DiaMes, year)
if err != nil {
log.Printf("Error parsing date %s for year %d: %v", fh.DiaMes, year, err)
continue
}
_, err = db.Exec("INSERT OR IGNORE INTO holidays (date, description) VALUES (?, ?)", fullDate, fh.NomeFeriado)
if err != nil {
log.Printf("Error inserting holiday %s - %s: %v", fullDate, fh.NomeFeriado, err)
}
}
}
}
log.Println("Finished scraping holidays.")
}
var monthMap = map[string]time.Month{
"janeiro": time.January,
"fevereiro": time.February,
"março": time.March,
"abril": time.April,
"maio": time.May,
"junho": time.June,
"julho": time.July,
"agosto": time.August,
"setembro": time.September,
"outubro": time.October,
"novembro": time.November,
"dezembro": time.December,
}
func parseFebrabanDate(dateStr string, year int) (string, error) {
parts := strings.Split(dateStr, " de ")
if len(parts) != 2 {
// Attempt to parse as DD/MM as a fallback
parsedDate, err := time.Parse("02/01", dateStr)
if err != nil {
return "", fmt.Errorf("unsupported date format: %s", dateStr)
}
return time.Date(year, parsedDate.Month(), parsedDate.Day(), 0, 0, 0, 0, time.UTC).Format("2006-01-02"), nil
}
day, err := strconv.Atoi(parts[0])
if err != nil {
return "", fmt.Errorf("invalid day: %s", parts[0])
}
monthName := strings.ToLower(parts[1])
month, ok := monthMap[monthName]
if !ok {
return "", fmt.Errorf("unknown month: %s", monthName)
}
return time.Date(year, month, day, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), nil
}
func getHolidaysHandler(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, date, description FROM holidays ORDER BY date ASC")
if err != nil {
http.Error(w, "Failed to query holidays", http.StatusInternalServerError)
log.Printf("Error querying holidays: %v", err)
return
}
defer rows.Close()
var holidays []Holiday
for rows.Next() {
var h Holiday
if err := rows.Scan(&h.ID, &h.Date, &h.Description); err != nil {
log.Printf("Error scanning holiday row: %v", err)
continue
}
holidays = append(holidays, h)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(holidays); err != nil {
log.Printf("Error encoding holidays to JSON: %v", err)
http.Error(w, "Failed to encode holidays", http.StatusInternalServerError)
}
}
func main() {
if err := initializeDB(); err != nil {
log.Fatalf("Error initializing database: %v", err)
}
// Initial scrape and periodic scraping
go func() {
scrapeHolidays() // Scrape on startup
ticker := time.NewTicker(24 * time.Hour) // Scrape every 24 hours
for range ticker.C {
scrapeHolidays()
}
}()
r := mux.NewRouter()
r.HandleFunc("/holidays", getHolidaysHandler).Methods("GET")
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Welcome to the Brazilian Bank Holidays API! Use /holidays to get the list.")
})
log.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
}
}
package main
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
)
// setupTestDB creates an in-memory SQLite database for testing.
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
// Using in-memory database for tests
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open in-memory database: %v", err)
}
createTableSQL := `CREATE TABLE holidays (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL UNIQUE,
description TEXT NOT NULL
);`
if _, err := db.Exec(createTableSQL); err != nil {
t.Fatalf("Failed to create holidays table: %v", err)
}
return db
}
func TestScrapeHolidays(t *testing.T) {
// 1. Setup mock HTTP server
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var holidays []FebrabanHoliday
// Respond based on the year requested
year := r.URL.Query().Get("ano")
if year == "2026" {
holidays = []FebrabanHoliday{
{DiaMes: "01 de janeiro", NomeFeriado: "Confraternização Universal"},
{DiaMes: "25 de dezembro", NomeFeriado: "Natal"},
}
} else if year == "2027" {
holidays = []FebrabanHoliday{
{DiaMes: "01 de janeiro", NomeFeriado: "Confraternização Universal 2027"},
}
}
json.NewEncoder(w).Encode(holidays)
}))
defer mockServer.Close()
// 2. Setup test database
tempDB := setupTestDB(t)
defer tempDB.Close()
// Set the global db variable to the test database and restore it after the test
originalDB := db
db = tempDB
defer func() { db = originalDB }()
// 3. Override doHTTPRequest and timeNow to point to the mock server and mock time
originalDoHTTPRequest := doHTTPRequest
defer func() { doHTTPRequest = originalDoHTTPRequest }()
doHTTPRequest = func(requestURL string) ([]byte, error) {
// Parse the requestURL to extract the 'ano' parameter
parsedURL, err := url.Parse(requestURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
year := parsedURL.Query().Get("ano")
// Construct a mock request to our httptest server
mockReq := httptest.NewRequest("GET", mockServer.URL+"?ano="+year, nil)
// Since httptest.NewServer creates a simple handler, we need to manually call it
// And capture the response
rr := httptest.NewRecorder()
mockServer.Config.Handler.ServeHTTP(rr, mockReq)
if rr.Code != http.StatusOK {
return nil, fmt.Errorf("mock server returned non-OK status: %d", rr.Code)
}
return rr.Body.Bytes(), nil
}
originalTimeNow := timeNow
defer func() { timeNow = originalTimeNow }()
timeNow = func() time.Time { return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) }
// 4. Run the scraper
scrapeHolidays()
// 5. Verify the results in the database
rows, err := db.Query("SELECT date, description FROM holidays")
if err != nil {
t.Fatalf("Failed to query test database: %v", err)
}
defer rows.Close()
var holidays []Holiday
for rows.Next() {
var h Holiday
if err := rows.Scan(&h.Date, &h.Description); err != nil {
t.Fatalf("Failed to scan row: %v", err)
}
holidays = append(holidays, h)
}
// We expect 3 holidays: two from 2026 (Confraternização Universal, Natal) and one from 2027.
expectedCount := 3
if len(holidays) != expectedCount {
t.Errorf("Expected %d holidays to be inserted, but got %d", expectedCount, len(holidays))
}
// Check for specific holidays
expectedHolidays := map[string]string{
"2026-01-01": "Confraternização Universal",
"2026-12-25": "Natal",
"2027-01-01": "Confraternização Universal 2027",
}
for _, h := range holidays {
expectedDesc, found := expectedHolidays[h.Date]
if !found {
t.Errorf("Found unexpected holiday: %v", h)
}
if expectedDesc != h.Description {
t.Errorf("For date %s, expected description '%s', got '%s'", h.Date, expectedDesc, h.Description)
}
delete(expectedHolidays, h.Date)
}
if len(expectedHolidays) > 0 {
t.Errorf("Missing expected holidays: %v", expectedHolidays)
}
}
func TestGetHolidaysHandler(t *testing.T) {
// 1. Setup test database and data
tempDB := setupTestDB(t)
defer tempDB.Close()
// Set the global db variable for the handler and restore it after the test
originalDB := db
db = tempDB
defer func() { db = originalDB }()
// Insert test data
testHolidays := []Holiday{
{Date: "2026-01-01", Description: "New Year"},
{Date: "2026-07-04", Description: "Independence Day"},
}
for _, h := range testHolidays {
_, err := db.Exec("INSERT INTO holidays (date, description) VALUES (?, ?)", h.Date, h.Description)
if err != nil {
t.Fatalf("Failed to insert test data: %v", err)
}
}
// 2. Create a request and response recorder
req := httptest.NewRequest("GET", "/holidays", nil)
rr := httptest.NewRecorder()
// 3. Call the handler
getHolidaysHandler(rr, req)
// 4. Check the results
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
// Check the response body
var returnedHolidays []Holiday
if err := json.NewDecoder(rr.Body).Decode(&returnedHolidays); err != nil {
t.Fatalf("Failed to decode response body: %v", err)
}
if len(returnedHolidays) != len(testHolidays) {
t.Errorf("Expected %d holidays, but got %d", len(testHolidays), len(returnedHolidays))
}
// Sort for consistent comparison, as database order isn't guaranteed without ORDER BY
// In getHolidaysHandler we use ORDER BY, so it should be consistent
if returnedHolidays[0].Description != "New Year" {
t.Errorf("Expected first holiday to be 'New Year', but got '%s'", returnedHolidays[0].Description)
}
}
// Global variable to allow mocking time.Now() - not needed in main_test.go as it's defined in main.go
// var timeNow = time.Now
func TestMain(m *testing.M) {
// This is a good place for setup/teardown if needed,
// but our tests are self-contained.
os.Exit(m.Run())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment