Skip to content

Commit 4097846

Browse files
dixneuf19Julen Dixneuf
andauthored
Add health check endpoint and resolve locale detection bug (#387)
* Add health check API endpoint with database connectivity * Update locale handling to fallback to default language on invalid input * Add health check endpoints for application readiness and liveness - Introduced `/api/health/readiness` endpoint to check if the application can serve requests, including database connectivity. - Introduced `/api/health/liveness` endpoint to verify if the application is running independently of external dependencies. - Updated the health check logic to streamline database connectivity checks and response handling. * Refactor health check logic --------- Co-authored-by: Julen Dixneuf <julen.d@padoa-group.com>
1 parent d27cbdb commit 4097846

6 files changed

Lines changed: 118 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ Here is the current state of translation:
7171
You could also pull it from the container registry:
7272
```docker pull ghcr.io/spliit-app/spliit:latest```
7373

74+
## Health check
75+
76+
The application has a health check endpoint that can be used to check if the application is running and if the database is accessible.
77+
78+
- `GET /api/health/readiness` or `GET /api/health` - Check if the application is ready to serve requests, including database connectivity.
79+
- `GET /api/health/liveness` - Check if the application is running, but not necessarily ready to serve requests.
80+
7481
## Opt-in features
7582

7683
### Expense documents
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { checkLiveness } from '@/lib/health'
2+
3+
// Liveness: Is the app itself healthy? (no external dependencies)
4+
// If this fails, Kubernetes should restart the pod
5+
export async function GET() {
6+
return checkLiveness()
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { checkReadiness } from '@/lib/health'
2+
3+
// Readiness: Can the app serve requests? (includes all external dependencies)
4+
// If this fails, Kubernetes should stop sending traffic but not restart
5+
export async function GET() {
6+
return checkReadiness()
7+
}

src/app/api/health/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { checkReadiness } from '@/lib/health'
2+
3+
// Default health check - same as readiness (includes database check)
4+
// This is readiness-focused for monitoring tools like uptime-kuma
5+
export async function GET() {
6+
return checkReadiness()
7+
}

src/lib/health.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { prisma } from '@/lib/prisma'
2+
3+
export interface HealthCheckStatus {
4+
status: 'healthy' | 'unhealthy'
5+
services?: {
6+
database?: {
7+
status: 'healthy' | 'unhealthy'
8+
error?: string
9+
}
10+
}
11+
}
12+
13+
async function checkDatabase(): Promise<{ status: 'healthy' | 'unhealthy'; error?: string }> {
14+
try {
15+
// Simple query to test database connectivity
16+
await prisma.$queryRaw`SELECT 1`
17+
return {
18+
status: 'healthy'
19+
}
20+
} catch (error) {
21+
return {
22+
status: 'unhealthy',
23+
error: error instanceof Error ? error.message : 'Database connection failed'
24+
}
25+
}
26+
}
27+
28+
function createHealthResponse(data: HealthCheckStatus, isHealthy: boolean): Response {
29+
return new Response(JSON.stringify(data), {
30+
status: isHealthy ? 200 : 503,
31+
headers: {
32+
'Cache-Control': 'no-cache, no-store, must-revalidate',
33+
'Content-Type': 'application/json'
34+
}
35+
})
36+
}
37+
38+
export async function checkReadiness(): Promise<Response> {
39+
try {
40+
const databaseStatus = await checkDatabase()
41+
42+
const services: HealthCheckStatus['services'] = {
43+
database: databaseStatus
44+
}
45+
46+
// For readiness: healthy only if all services are healthy
47+
const isHealthy = databaseStatus.status === 'healthy'
48+
49+
const healthStatus: HealthCheckStatus = {
50+
status: isHealthy ? 'healthy' : 'unhealthy',
51+
services
52+
}
53+
54+
return createHealthResponse(healthStatus, isHealthy)
55+
} catch (error) {
56+
const errorStatus: HealthCheckStatus = {
57+
status: 'unhealthy',
58+
services: {
59+
database: {
60+
status: 'unhealthy',
61+
error: error instanceof Error ? error.message : 'Readiness check failed'
62+
}
63+
}
64+
}
65+
66+
return createHealthResponse(errorStatus, false)
67+
}
68+
}
69+
70+
export async function checkLiveness(): Promise<Response> {
71+
try {
72+
// Liveness: Only check if the app process is alive
73+
// No database or external service checks - restarting won't fix those
74+
const healthStatus: HealthCheckStatus = {
75+
status: 'healthy'
76+
// No services reported - we don't check them for liveness
77+
}
78+
79+
return createHealthResponse(healthStatus, true) // Always 200 for liveness
80+
} catch (error) {
81+
// This should rarely happen, but if it does, the app needs restart
82+
const errorStatus: HealthCheckStatus = {
83+
status: 'unhealthy'
84+
}
85+
86+
return createHealthResponse(errorStatus, false)
87+
}
88+
}

src/lib/locale.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ function getAcceptLanguageLocale(requestHeaders: Headers, locales: Locales) {
1717
try {
1818
locale = match(languages, locales, defaultLocale)
1919
} catch (e) {
20-
// invalid language
20+
// invalid language - fallback to default
21+
locale = defaultLocale
2122
}
2223
return locale
2324
}

0 commit comments

Comments
 (0)