A high-performance health check API built with Hono, supporting both Node.js and Cloudflare Workers runtimes.
- 🚀 Dual Runtime: Runs on Node.js (with better-sqlite3) and Cloudflare Workers (with D1)
- 🔍 HTTP Health Checks: Verify endpoints are responding with expected status codes
- 🔌 TCP Port Checks: Test if ports are open and accepting connections (Node.js only)
- 🌐 Subdomain Support: Check health of subdomains like
api.example.com - 📁 Path Support: Check specific paths like
/api/v1/health - 🔒 API Token Authentication: Secure your endpoints with Bearer tokens
- ⏱️ Rate Limiting: Built-in rate limiting with configurable limits
- 💾 Persistent Storage: Store check results in SQLite/D1 for later retrieval
- 🛡️ SSRF Protection: Blocks requests to private IP ranges by default
- Node.js 18+ (for Node.js runtime)
- pnpm (recommended) or npm
pnpm installCopy .env.example to .env and configure:
PORT=3000
DB_URL=file:./data/app.db
API_TOKEN=your-secure-api-token-here
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=100
MAX_TARGETS_PER_REQUEST=10
TCP_TIMEOUT_MS=5000
HTTP_TIMEOUT_MS=10000# Development (Node.js)
pnpm run dev:node
# Development (Workers - requires wrangler)
pnpm run dev:worker
# Build for production
pnpm run buildAll endpoints require authentication via Bearer token:
Authorization: Bearer <your-api-token>
Check if the API is running.
GET /v1/pingResponse:
{
"success": true,
"data": {
"message": "pong",
"timestamp": "2024-01-01T00:00:00.000Z",
"mode": "node"
}
}Perform health checks without storing results.
POST /v1/check
Content-Type: application/jsonRequest Body:
{
"targets": [
{
"host": "example.com",
"port": 443,
"protocol": "https",
"subdomain": "api",
"path": "/v1/health",
"method": "HEAD",
"timeout": 5000,
"checkTcp": true
}
]
}Response:
{
"success": true,
"data": {
"results": [
{
"target": {
"host": "example.com",
"port": 443,
"protocol": "https",
"subdomain": "api",
"path": "/v1/health"
},
"isAlive": true,
"portAlive": true,
"http": {
"statusCode": 200,
"statusText": "OK",
"responseTimeMs": 150
},
"tcp": {
"connected": true,
"responseTimeMs": 45
},
"totalMs": 200,
"checkedAt": "2024-01-01T00:00:00.000Z"
}
]
}
}Perform health checks and store results for later retrieval.
POST /v1/checks
Content-Type: application/jsonRequest Body: Same as /v1/check
Response:
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"targetsCount": 1,
"createdAt": "2024-01-01T00:00:00.000Z"
}
}Retrieve previously stored check results.
GET /v1/checks/:idResponse: Full check results including all target check details.
Delete a stored check and its results.
DELETE /v1/checks/:idResponse:
{
"success": true,
"data": {
"message": "Check deleted successfully",
"id": "550e8400-e29b-41d4-a716-446655440000"
}
}| Field | Type | Default | Description |
|---|---|---|---|
host |
string | required | Domain or IP address |
port |
number | 443 | Port number (1-65535) |
protocol |
string | "https" | "http" or "https" |
subdomain |
string | - | Optional subdomain (e.g., "api" → "api.example.com") |
path |
string | "/" | URL path to check |
method |
string | "HEAD" | HTTP method (GET, HEAD, POST, PUT, DELETE) |
timeout |
number | 10000 | Request timeout in milliseconds |
checkTcp |
boolean | true | Perform TCP port check |
curl -X POST http://localhost:3000/v1/check \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-token" \
-d '{
"targets": [
{ "host": "google.com", "port": 443, "protocol": "https" }
]
}'curl -X POST http://localhost:3000/v1/check \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-token" \
-d '{
"targets": [
{
"host": "example.com",
"port": 443,
"protocol": "https",
"subdomain": "api",
"path": "/v1/status"
}
]
}'curl -X POST http://localhost:3000/v1/check \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-token" \
-d '{
"targets": [
{ "host": "github.com", "port": 443, "protocol": "https" },
{ "host": "gitlab.com", "port": 443, "protocol": "https" },
{ "host": "bitbucket.org", "port": 443, "protocol": "https" }
]
}'Default rate limits:
- 100 requests per 60 seconds per IP
Rate limit headers are included in all responses:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Requests remaining in windowX-RateLimit-Reset: Unix timestamp when the window resets
All errors follow this format:
{
"success": false,
"error": {
"message": "Error description",
"code": "ERROR_CODE"
}
}Common error codes:
UNAUTHORIZED: Missing or invalid API tokenRATE_LIMIT_EXCEEDED: Too many requestsVALIDATION_ERROR: Invalid request bodySSRF_VIOLATION: Attempted to access private IPNOT_FOUND: Resource not found
The /data/ directory contains the SQLite database file. It's important to protect this path from public access.
🔶 Cloudflare Workers
In Cloudflare Workers, the /data/ path doesn't exist as files are not served directly. However, if you're using Cloudflare Pages or need to block specific paths:
Option 1: Using _routes.json (Cloudflare Pages)
Create public/_routes.json:
{
"version": 1,
"exclude": ["/data/*"]
}Option 2: Block in Worker code
Add middleware in your worker:
import { Hono } from 'hono'
const app = new Hono()
// Block /data/ path
app.use('/data/*', (c) => {
return c.text('Forbidden', 403)
})Option 3: Cloudflare WAF Rules
In Cloudflare Dashboard → Security → WAF → Custom Rules:
(http.request.uri.path contains "/data/")
Action: Block
🟢 Nginx
Add to your Nginx server block:
server {
listen 80;
server_name yourdomain.com;
# Block access to /data/ directory
location /data/ {
deny all;
return 403;
}
# Alternative: Return 404 (hide existence)
location /data/ {
return 404;
}
# Block specific file types in data directory
location ~* ^/data/.*\.(db|sqlite|sqlite3)$ {
deny all;
return 403;
}
# Your main application
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}Reload Nginx:
sudo nginx -t && sudo systemctl reload nginx🟠 Apache (.htaccess)
Create or edit .htaccess in your project root:
# Block access to /data/ directory
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^data(/.*)?$ - [F,L]
</IfModule>
# Alternative: Using Directory directive
<Directory "/path/to/your/project/data">
Order deny,allow
Deny from all
</Directory>
# Block specific file extensions
<FilesMatch "\.(db|sqlite|sqlite3)$">
Order allow,deny
Deny from all
</FilesMatch>For Apache virtual host configuration:
<VirtualHost *:80>
ServerName yourdomain.com
DocumentRoot /var/www/html
# Block /data/ directory
<Location /data>
Order deny,allow
Deny from all
</Location>
# Or return 404
<Location /data>
Redirect 404 /
</Location>
</VirtualHost>Restart Apache:
sudo apachectl configtest && sudo systemctl restart apache2🔵 Docker / Node.js (Express/Hono)
If serving static files, ensure /data/ is excluded:
import { Hono } from 'hono'
import { serveStatic } from 'hono/serve-static'
const app = new Hono()
// Block /data/ before static serving
app.use('/data/*', (c) => {
return c.json({ error: 'Forbidden' }, 403)
})
// Serve static files (excluding /data/)
app.use('/*', serveStatic({ root: './public' }))Best Practice: Don't put the data/ folder in a publicly served directory. Keep it outside your web root:
project/
├── data/ # ← Outside public folder
│ └── app.db
├── public/ # ← Web root
│ └── index.html
└── src/
- Cloudflare account
- Wrangler CLI installed
npx wrangler login# Create the database
npx wrangler d1 create domain-health
# Copy the database_id from output and update wrangler.tomlReplace placeholder IDs with your actual values:
[[d1_databases]]
binding = "DB"
database_name = "domain-health"
database_id = "your-actual-database-id" # From step 2npx wrangler d1 execute domain-health --remote --file=./drizzle/0000_init.sqlnpx wrangler secret put API_TOKEN
# Enter your secure API token when prompted# Deploy to production
pnpm run deploy:worker
# or
npx wrangler deployYour API will be available at https://hono-health-api.<your-subdomain>.workers.dev
Push your code to a GitHub repository.
Go to Settings → Secrets and variables → Actions and add:
| Secret Name | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
Create API Token with "Edit Cloudflare Workers" permission |
CLOUDFLARE_ACCOUNT_ID |
Found in Workers & Pages dashboard |
API_TOKEN |
Your secure API token for the health check API |
Create .github/workflows/deploy.yml:
name: Deploy to Cloudflare Workers
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
API_TOKEN
env:
API_TOKEN: ${{ secrets.API_TOKEN }}git add .
git commit -m "Add GitHub Actions workflow"
git push origin mainThe workflow will automatically deploy on every push to main.
# Start local Workers dev server
pnpm run dev:worker
# This uses wrangler dev with local D1Set via wrangler.toml (non-sensitive) or wrangler secret (sensitive):
| Variable | How to Set | Description |
|---|---|---|
API_TOKEN |
wrangler secret put API_TOKEN |
API authentication token |
MAX_TARGETS_PER_REQUEST |
wrangler.toml [vars] |
Max targets per request |
RATE_LIMIT_WINDOW_MS |
wrangler.toml [vars] |
Rate limit window |
RATE_LIMIT_MAX_REQUESTS |
wrangler.toml [vars] |
Max requests per window |
hono-health-api/
├── src/
│ ├── app.ts # Main Hono application with routes
│ ├── index.node.ts # Node.js entry point
│ ├── index.worker.ts # Cloudflare Workers entry point
│ ├── db/
│ │ ├── index.ts # Database adapter (SQLite/D1)
│ │ ├── schema.ts # Drizzle ORM schema
│ │ └── migrate.node.ts # Node.js migration script
│ ├── middleware/
│ │ └── errorHandler.ts # Error handling middleware
│ ├── types/
│ │ └── index.ts # TypeScript interfaces
│ └── utils/
│ ├── healthCheck.ts # HTTP/TCP health check logic
│ ├── security.ts # SSRF protection & validation
│ └── validation.ts # Zod schemas & input validation
├── data/
│ └── app.db # SQLite database (Node.js)
├── drizzle/
│ └── *.sql # Generated migrations
├── .env.example # Environment variables template
├── drizzle.config.ts # Drizzle Kit configuration
├── package.json # Dependencies & scripts
├── tsconfig.json # TypeScript configuration
├── wrangler.toml # Cloudflare Workers configuration
├── Dockerfile # Docker build configuration
└── docker-compose.yml # Docker Compose setup
flowchart TB
subgraph Client
A[API Client]
end
subgraph "Hono Health API"
subgraph Middleware
B[CORS]
C[Bearer Auth]
D[Rate Limiter]
E[Error Handler]
end
subgraph Routes
F["/v1/ping"]
G["/v1/check"]
H["/v1/checks"]
I["GET /v1/checks/:id"]
J["DELETE /v1/checks/:id"]
end
subgraph Utils
K[Health Check]
L[Validation]
M[Security]
end
subgraph Database
N["SQLite (Node.js)"]
O["D1 (Workers)"]
end
end
subgraph "External Targets"
P[Target Servers]
end
A --> B --> C --> D --> E
E --> F & G & H & I & J
G & H --> L --> M --> K
K --> P
H & I & J --> N & O
sequenceDiagram
participant C as Client
participant A as API
participant V as Validator
participant S as Security
participant H as HealthCheck
participant T as Target
participant DB as Database
C->>A: POST /v1/checks
A->>A: Auth & Rate Limit
A->>V: Validate Input
V->>S: SSRF Check
S-->>A: Approved
loop For each target
A->>H: Perform Check
H->>T: TCP Connect
T-->>H: Port Status
H->>T: HTTP Request
T-->>H: Response
H-->>A: Result
end
A->>DB: Store Results
A-->>C: Return Check ID
This project was built with the assistance of AI:
| Contribution | AI Model | Tool |
|---|---|---|
| 🏗️ Initial Build | Kimi K2.5 Thinking | Agent Website |
| 🔒 Error Fixes & Security | Claude Opus 4.5 (Thinking) | Antigravity IDE |
| ♻️ Refactoring | Claude Opus 4.5 (Thinking) | Antigravity IDE |
Human oversight and manual review by the project maintainer.
MIT