A lightweight, zero-dependency REST API for Indonesian public holidays and collective leave days, built with Go standard library only.
Tanggal merah (lit. red date) is the Indonesian term for a public holiday — the days marked in red on a calendar.
- Query holidays by year, year + month, or specific date
- Two types:
holiday(hari libur nasional) andleave(cuti bersama) - Date check endpoint returns
is_holiday,is_leave, andis_off_day yeardefaults to the current year when omitted- In-memory cache per year — fast after first load
- Graceful shutdown, structured JSON logging, CORS-ready
- Multi-stage Docker build producing a minimal
scratchimage - Full test suite (
go test ./...) - Zero external dependencies — Go 1.22 stdlib only
Showcasing who is using the Tanggal Merah API:
git clone https://github.com/fransallen/tanggalmerah.git
cd tanggalmerah
air
# http://localhost:8080docker build -t tanggalmerah .
docker run -p 8080:8080 tanggalmerah| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Port the HTTP server listens on |
DATA_DIR |
data |
Directory containing YEAR.json files |
Base URL: http://localhost:8080
All successful responses:
{
"success": true,
"data": "...",
"meta": {
"total": 25,
"total_holidays": 17,
"total_leave": 8,
"year": 2026
}
}All error responses:
{ "success": false, "error": "Human-readable message", "code": "ERROR_CODE" }type |
Description |
|---|---|
holiday |
Hari Libur Nasional — mandatory day off |
leave |
Cuti Bersama — government-designated bridge/leave day |
Redirects to the web interface for the current year.
curl http://localhost:8080/health{ "status": "ok", "version": "1.0.0", "time": "2026-01-01T00:00:00Z" }List all years that have available data.
curl http://localhost:8080/api/years{
"success": true,
"data": [2023, 2024, 2025, 2026, 2027],
"meta": { "total": 5 }
}Query holidays by year and optional month, with type filters.
| Query param | Values | Required | Default |
|---|---|---|---|
year |
1900–2100 |
No | current year |
month |
1–12 |
No | — |
type |
holiday | leave | all |
No | all |
# All holidays for the current year
curl "http://localhost:8080/api/holidays"
# All 2026 holidays
curl "http://localhost:8080/api/holidays?year=2026"
# March only
curl "http://localhost:8080/api/holidays?year=2026&month=3"
# Only collective leave days in March
curl "http://localhost:8080/api/holidays?year=2026&month=3&type=leave"Example response:
{
"success": true,
"data": [
{
"date": "2026-03-18",
"day": "Rabu",
"name": "Hari Suci Nyepi Tahun Baru Saka 1948",
"type": "leave"
}
],
"meta": {
"total": 1,
"total_holidays": 0,
"total_leave": 1,
"year": 2026,
"month": 3,
"source": "Government of the Republic of Indonesia"
}
}Error codes
| Code | HTTP | When |
|---|---|---|
INVALID_YEAR |
400 | Year is not a valid integer (1900–2100) |
INVALID_MONTH |
400 | Month is not 1–12 |
INVALID_TYPE |
400 | Type is not one of the accepted values |
YEAR_NOT_FOUND |
404 | No data file exists for that year |
Check whether a specific date is a public holiday, collective leave day, or a regular workday.
curl "http://localhost:8080/api/check?date=2026-03-21"{
"success": true,
"data": {
"date": "2026-03-21",
"is_holiday": true,
"is_leave": false,
"is_off_day": true,
"holidays": [
{
"date": "2026-03-21",
"day": "Sabtu",
"name": "Hari Raya Idul Fitri 1447 Hijriyah",
"type": "holiday"
}
]
}
}curl "http://localhost:8080/api/check?date=2026-03-20"{
"success": true,
"data": {
"date": "2026-03-20",
"is_holiday": false,
"is_leave": true,
"is_off_day": true,
"holidays": [
{
"date": "2026-03-20",
"day": "Jumat",
"name": "Hari Raya Idul Fitri 1447 Hijriyah",
"type": "leave"
}
]
}
}curl "http://localhost:8080/api/check?date=2026-03-25"{
"success": true,
"data": {
"date": "2026-03-25",
"is_holiday": false,
"is_leave": false,
"is_off_day": false
}
}If no data file exists for the year of the requested date, the endpoint returns
200with all flagsfalserather than404.
Error codes
| Code | HTTP | When |
|---|---|---|
MISSING_DATE |
400 | date query param is absent |
INVALID_DATE |
400 | date is not in YYYY-MM-DD format |
Each year's data lives in data/YYYY.json:
{
"year": 2026,
"holidays": [
{
"date": "2026-01-01",
"day": "Kamis",
"name": "Tahun Baru 2026 Masehi",
"type": "holiday"
},
{
"date": "2026-12-24",
"day": "Kamis",
"name": "Hari Raya Natal",
"type": "leave"
}
]
}To add a new year: drop a YYYY.json file into the data/ folder and restart the server.
tanggalmerah/
├── cmd/server/
│ ├── main.go # Entry point, router, middleware, graceful shutdown
│ └── main_test.go # Tests for middleware and helpers
├── internal/
│ ├── handler/
│ │ ├── handler.go # HTTP handlers
│ │ └── handler_test.go # Tests
│ ├── model/
│ │ └── model.go # Domain types and response envelopes
│ └── repository/
│ ├── repository.go # JSON loading, in-memory cache, filter helpers
│ └── repository_test.go # Tests
├── data/
│ ├── 2023.json
│ ├── 2024.json
│ ├── 2025.json
│ └── 2026.json
├── .air.toml # Live-reload config (air)
├── Dockerfile
├── go.mod
├── package.json
└── README.md
go test ./...
go test ./... -v -racePull requests are welcome! The most valuable contribution is adding official data for years not yet covered.
- Fork the repository
- Create
data/YYYY.jsonfollowing the schema above - Verify the data against the official government decree (Surat Keputusan Bersama)
- Include both
holidayandleaveentries where applicable - Open a pull request
MIT