Skip to content

Add /api/health endpoint with MongoDB/Redis dependency checks - #86

Merged
Shivkumardhakad merged 3 commits into
mainfrom
copilot/add-api-health-endpoint
Apr 9, 2026
Merged

Shivkumardhakad merged 3 commits into
mainfrom
copilot/add-api-health-endpoint

Conversation

Copilot AI commented Apr 9, 2026 •

Copy link
Copy Markdown
Contributor

This PR introduces a dedicated GET /api/health endpoint for external monitoring to quickly assess API readiness and dependency connectivity. The endpoint reports health for MongoDB and Redis and returns a machine-readable status payload.

  • Health endpoint

    • Added GET /api/health route in apps/public-api.
    • Mounted under existing middleware chain (limiter, logger) to match route protection patterns.
  • Dependency status evaluation

    • Added health controller logic to evaluate:
      • MongoDB connectivity via mongoose.connection.readyState === 1
      • Redis connectivity via redis.status === "ready" plus redis.ping()
    • Added a short Redis ping timeout guard to avoid hanging health responses when Redis is unresponsive.
  • Response shape

    • Returns structured health payload with:
      • status (ok / error)
      • timestamp (ISO string)
      • dependencies.mongodb and dependencies.redis (connected / disconnected)
    • Uses 503 when one or more dependencies are unhealthy.
  • Focused coverage

    • Added route-level tests for:
      • all dependencies healthy
      • MongoDB disconnected
      • Redis unresponsive
return res.status(status === 'ok' ? 200 : 503).json({
  status,
  timestamp: new Date().toISOString(),
  dependencies: {
    mongodb: isMongoConnected ? 'connected' : 'disconnected',
    redis: isRedisConnected ? 'connected' : 'disconnected',
  },
});

@yash-pouranik
yash-pouranik temporarily deployed to copilot/add-api-health-endpoint - urBackend-frankfrut PR #86 April 9, 2026 09:10 — with Render Destroyed
@yash-pouranik
yash-pouranik temporarily deployed to copilot/add-api-health-endpoint - urBackend-frankfrut PR #86 April 9, 2026 09:12 — with Render Destroyed
Copilot AI changed the title [WIP] Add /api/health endpoint for system monitoring Add /api/health endpoint with MongoDB/Redis dependency checks Apr 9, 2026
@Shivkumardhakad
Shivkumardhakad marked this pull request as ready for review April 9, 2026 09:20
Copilot AI review requested due to automatic review settings April 9, 2026 09:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new GET /api/health endpoint in apps/public-api to report API readiness by checking MongoDB and Redis connectivity and returning a structured JSON payload (200 when healthy, 503 when unhealthy).

Changes:

  • Added /api/health router and mounted it in the public API app.
  • Implemented health controller logic for MongoDB (mongoose.connection.readyState) and Redis (redis.status + ping() with timeout).
  • Added route-level Jest/Supertest coverage for healthy, Mongo disconnected, and Redis error scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
apps/public-api/src/routes/health.js New router exposing GET / for the health endpoint.
apps/public-api/src/controllers/health.controller.js Health evaluation logic for MongoDB + Redis with a ping timeout guard and structured response.
apps/public-api/src/app.js Mounts the new health route under /api/health with existing middleware.
apps/public-api/src/tests/health.route.test.js Adds tests validating status codes and dependency states.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +11 to +15
const pingResponse = await Promise.race([
redis.ping(),
new Promise((resolve) => setTimeout(() => resolve('TIMEOUT'), REDIS_PING_TIMEOUT_MS)),
]);
isRedisConnected = pingResponse === 'PONG';

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Redis ping timeout is implemented with Promise.race + setTimeout, but the timeout timer is never cleared when redis.ping() resolves first. Under frequent health checks this leaves many unnecessary timers pending (and the ping promise can also continue running after the timeout wins), which can add avoidable event-loop overhead and resource usage. Consider creating an explicit timeout handle, unref() it, and clearTimeout() it in a finally block (and, if possible with the Redis client, use a cancellable/command-timeout mechanism instead of leaving in-flight pings running).

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +74
test('returns error when redis is not responsive', async () => {
mockRedis.ping.mockRejectedValue(new Error('redis unavailable'));

const res = await request(app).get('/api/health');

expect(res.status).toBe(503);
expect(res.body.status).toBe('error');
expect(res.body.dependencies).toEqual({
mongodb: 'connected',
redis: 'disconnected',
});
});
});

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The controller introduces a Redis ping timeout guard, but the tests only cover the case where ping() rejects. This doesn't verify the intended behavior when ping() hangs (the case the timeout is meant to protect against). Add a test where mockRedis.ping returns a never-resolving promise and assert the endpoint still responds (e.g., using Jest fake timers / advancing time) and marks Redis as disconnected.

Copilot uses AI. Check for mistakes.
@Shivkumardhakad

Copy link
Copy Markdown
Collaborator

@copilot apply changes based on the comments in this thread

@Shivkumardhakad

Copy link
Copy Markdown
Collaborator

@coderabbitai ok to merge?

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR details, status checks, and open review comments
echo "=== PR STATUS ==="
gh pr view 86 --repo geturbackend/urBackend --json state,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup

echo ""
echo "=== OPEN REVIEW COMMENTS ==="
gh pr view 86 --repo geturbackend/urBackend --json reviews,comments

echo ""
echo "=== PR CHECKS ==="
gh pr checks 86 --repo geturbackend/urBackend

echo ""
echo "=== REVIEW THREADS ==="
gh api graphql -f query='
{
  repository(owner: "geturbackend", name: "urBackend") {
    pullRequest(number: 86) {
      reviewThreads(first: 20) {
        nodes {
          isResolved
          isOutdated
          comments(first: 5) {
            nodes {
              author { login }
              body
              createdAt
            }
          }
        }
      }
    }
  }
}'

Length of output: 6098


@Shivkumardhakad — All CI checks are green and the branch is clean, but there are 2 unresolved review threads that should be addressed before merging:


1. 🔴 Redis ping timeout — timer is never cleared (health.controller.js)

The Promise.race timeout guard never clears the setTimeout when redis.ping() resolves first. Under frequent health checks this accumulates pending timers and in-flight pings, adding avoidable event-loop overhead.

Suggested fix:

async function isRedisHealthy(redis, timeoutMs = 3000) {
  let timeoutHandle;
  const timeoutPromise = new Promise((_, reject) => {
    timeoutHandle = setTimeout(() => reject(new Error('Redis ping timed out')), timeoutMs);
    timeoutHandle.unref?.(); // don't block process exit
  });

  try {
    await Promise.race([redis.ping(), timeoutPromise]);
    return true;
  } catch {
    return false;
  } finally {
    clearTimeout(timeoutHandle); // always clear, regardless of outcome
  }
}

2. 🔴 Missing test for the hang scenario (health.route.test.js)

Tests only cover ping() rejecting, but not ping() hanging indefinitely — which is the exact case the timeout guard is meant to protect against. This leaves the primary guard behavior unverified.

Suggested test using Jest fake timers:

it('returns 503 when Redis ping hangs (timeout)', async () => {
  mockMongoose.connection.readyState = 1;
  mockRedis.status = 'ready';
  // ping never resolves
  mockRedis.ping.mockReturnValue(new Promise(() => {}));

  jest.useFakeTimers();

  const responsePromise = request(app).get('/api/health');
  jest.advanceTimersByTime(3001); // advance past the timeout

  const response = await responsePromise;

  expect(response.status).toBe(503);
  expect(response.body.dependencies.redis).toBe('disconnected');

  jest.useRealTimers();
});

Summary

Check Status
CI (lint, tests, CodeQL) ✅ All passing
Branch state ✅ Clean, no conflicts
Open review threads ❌ 2 unresolved
Formal approval ⚠️ None recorded

Recommendation: not yet ready to merge. Please address the two items above (or consciously decide to defer them) before proceeding.

@Shivkumardhakad

Copy link
Copy Markdown
Collaborator

@coderabbitai please create followup issue for this
till then I merge this

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo geturbackend/urBackend \
  --title "health endpoint: clear Redis ping timeout timer & add hang-scenario test" \
  --assignee Shivkumardhakad \
  --body "## Follow-up from PR `#86`

Two items were deferred from the \`/api/health\` endpoint PR (https://github.com/geturbackend/urBackend/pull/86) to keep the merge unblocked.

---

### 1. Redis ping timeout — timer is never cleared (\`health.controller.js\`)

The \`Promise.race\` timeout guard never clears the \`setTimeout\` when \`redis.ping()\` resolves first. Under frequent health checks this accumulates pending timers and in-flight pings, adding avoidable event-loop overhead.

**Suggested fix:**

\`\`\`js
async function isRedisHealthy(redis, timeoutMs = 3000) {
  let timeoutHandle;
  const timeoutPromise = new Promise((_, reject) => {
    timeoutHandle = setTimeout(() => reject(new Error('Redis ping timed out')), timeoutMs);
    timeoutHandle.unref?.(); // don't block process exit
  });

  try {
    await Promise.race([redis.ping(), timeoutPromise]);
    return true;
  } catch {
    return false;
  } finally {
    clearTimeout(timeoutHandle); // always clear, regardless of outcome
  }
}
\`\`\`

---

### 2. Missing test for the Redis ping hang scenario (\`health.route.test.js\`)

Tests only cover \`ping()\` rejecting, but not \`ping()\` hanging indefinitely — which is the exact case the timeout guard is meant to protect against. This leaves the primary guard behavior unverified.

**Suggested test using Jest fake timers:**

\`\`\`js
it('returns 503 when Redis ping hangs (timeout)', async () => {
  mockMongoose.connection.readyState = 1;
  mockRedis.status = 'ready';
  // ping never resolves
  mockRedis.ping.mockReturnValue(new Promise(() => {}));

  jest.useFakeTimers();

  const responsePromise = request(app).get('/api/health');
  jest.advanceTimersByTime(3001); // advance past the timeout

  const response = await responsePromise;

  expect(response.status).toBe(503);
  expect(response.body.dependencies.redis).toBe('disconnected');

  jest.useRealTimers();
});
\`\`\`

---

Raised by `@coderabbitai` on behalf of `@Shivkumardhakad`.
Review thread: https://github.com/geturbackend/urBackend/pull/86#pullrequestreview-4081163136
"

Length of output: 51


@Shivkumardhakad — Done! Follow-up issue created and assigned to you: #87

It tracks both deferred items:

  1. Clear the Redis ping setTimeout timer in health.controller.js to avoid event-loop overhead under frequent health checks.
  2. Add a hang-scenario test in health.route.test.js using Jest fake timers to verify the timeout guard actually fires.

Happy merging! 🐇

@Shivkumardhakad
Shivkumardhakad self-requested a review April 9, 2026 09:52
@Shivkumardhakad
Shivkumardhakad merged commit f19ad57 into main Apr 9, 2026
8 checks passed
Copilot AI requested a review from Shivkumardhakad April 9, 2026 10:06

This branch was successfully deployed

No deployments
copilot/add-api-health-endpoint - urBackend-frankfrut PR #86 — 4a3b6852 Deployed Apr 9, 2026 by yash-pouranik
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a /api/health Endpoint for System Monitoring

4 participants