diff --git a/.changeset/gentle-dryers-smile.md b/.changeset/gentle-dryers-smile.md
new file mode 100644
index 0000000000..b061461da1
--- /dev/null
+++ b/.changeset/gentle-dryers-smile.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-defaults': patch
+---
+
+The `createHealthRouter` utility that allows you to create a health check router is now exported via `@backstage/backend-defaults/rootHttpRouter`.
diff --git a/.changeset/soft-gorillas-refuse.md b/.changeset/soft-gorillas-refuse.md
new file mode 100644
index 0000000000..323c4d20d2
--- /dev/null
+++ b/.changeset/soft-gorillas-refuse.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-test-utils': patch
+---
+
+The default services for `startTestBackend` and `ServiceFactoryTester` now includes the Root Health Service.
diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md
index 329398a7b8..36f3aa3ab6 100644
--- a/packages/backend-defaults/api-report-rootHttpRouter.md
+++ b/packages/backend-defaults/api-report-rootHttpRouter.md
@@ -3,6 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+///
///
import { Config } from '@backstage/config';
@@ -17,10 +18,17 @@ import { LoggerService } from '@backstage/backend-plugin-api';
import { RequestHandler } from 'express';
import { RequestListener } from 'http';
import { RootConfigService } from '@backstage/backend-plugin-api';
+import { RootHealthService } from '@backstage/backend-plugin-api';
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
+import { Router } from 'express';
import type { Server } from 'node:http';
import { ServiceFactory } from '@backstage/backend-plugin-api';
+// @public (undocumented)
+export function createHealthRouter(options: {
+ health: RootHealthService;
+}): Router;
+
// @public
export function createHttpServer(
listener: RequestListener,
diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts
index 007d1dd9cd..3772ab0fab 100644
--- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts
+++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts
@@ -1,5 +1,3 @@
-import { RootHealthService } from '@backstage/backend-plugin-api';
-
/*
* Copyright 2024 The Backstage Authors
*
@@ -16,9 +14,13 @@ import { RootHealthService } from '@backstage/backend-plugin-api';
* limitations under the License.
*/
+import { RootHealthService } from '@backstage/backend-plugin-api';
import Router from 'express-promise-router';
import { Request, Response } from 'express';
+/**
+ * @public
+ */
export function createHealthRouter(options: { health: RootHealthService }) {
const router = Router();
diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts
index 9c2d96dab2..e9aee8a0e0 100644
--- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts
+++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts
@@ -18,6 +18,7 @@ export {
DefaultRootHttpRouter,
type DefaultRootHttpRouterOptions,
} from './DefaultRootHttpRouter';
+export { createHealthRouter } from './createHealthRouter';
export * from './http';
export {
rootHttpRouterServiceFactory,
diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts
new file mode 100644
index 0000000000..dea0159dff
--- /dev/null
+++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ServiceFactoryTester,
+ mockServices,
+} from '@backstage/backend-test-utils';
+import { Express } from 'express';
+import request from 'supertest';
+import { rootHttpRouterServiceFactory } from './rootHttpRouterServiceFactory';
+import { coreServices } from '@backstage/backend-plugin-api';
+
+describe('rootHttpRouterServiceFactory', () => {
+ it('should make the health endpoints available', async () => {
+ let app: Express | undefined = undefined;
+
+ const tester = ServiceFactoryTester.from(
+ rootHttpRouterServiceFactory({
+ configure(options) {
+ options.applyDefaults();
+ app = options.app;
+ },
+ }),
+ {
+ dependencies: [
+ mockServices.rootConfig.factory({
+ data: {
+ app: { baseUrl: 'http://localhost' },
+ backend: {
+ baseUrl: 'http://localhost',
+ listen: { host: '', port: 0 },
+ },
+ },
+ }),
+ ],
+ },
+ );
+
+ // Trigger creation of the http service, accessing the app instance through the configure callback
+ await tester.getSubject();
+
+ await request(app!)
+ .get('/.backstage/health/v1/liveness')
+ .expect(200, { status: 'ok' });
+
+ await request(app!).get('/.backstage/health/v1/readiness').expect(503, {
+ message: 'Backend has not started yet',
+ status: 'error',
+ });
+
+ const lifecycle = await tester.getService(coreServices.rootLifecycle);
+
+ await (lifecycle as any).startup(); // Trigger startup by calling the private startup method
+
+ await request(app!).get('/.backstage/health/v1/readiness').expect(200, {
+ status: 'ok',
+ });
+
+ expect('test').toBe('test');
+ });
+});
diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
index ecba34290d..a01d4bebf5 100644
--- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
+++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts
@@ -242,6 +242,15 @@ describe('TestBackend', () => {
expect(res.body).toEqual({ message: 'pong' });
});
+ it('should expose health check endpoints', async () => {
+ const { server } = await startTestBackend({ features: [] });
+
+ const res = await request(server).get('/.backstage/health/v1/liveness');
+
+ expect(res.status).toEqual(200);
+ expect(res.body).toEqual({ status: 'ok' });
+ });
+
it('should provide extension point implementations', async () => {
expect.assertions(3);
diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts
index 16ee6b78d2..3d662a8c12 100644
--- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts
+++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts
@@ -37,6 +37,7 @@ import express from 'express';
// Direct internal import to avoid duplication
// eslint-disable-next-line @backstage/no-forbidden-package-imports
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
+import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter';
/** @public */
export interface TestBackendOptions {
@@ -73,6 +74,7 @@ export const defaultServiceFactories = [
mockServices.lifecycle.factory(),
mockServices.logger.factory(),
mockServices.permissions.factory(),
+ mockServices.rootHealth.factory(),
mockServices.rootLifecycle.factory(),
mockServices.rootLogger.factory(),
mockServices.scheduler.factory(),
@@ -251,15 +253,18 @@ export async function startTestBackend(
config: coreServices.rootConfig,
lifecycle: coreServices.rootLifecycle,
rootLogger: coreServices.rootLogger,
+ health: coreServices.rootHealth,
},
- async factory({ config, lifecycle, rootLogger }) {
+ async factory({ config, lifecycle, rootLogger, health }) {
const router = DefaultRootHttpRouter.create();
const logger = rootLogger.child({ service: 'rootHttpRouter' });
const app = express();
const middleware = MiddlewareFactory.create({ config, logger });
+ const healthRouter = createHealthRouter({ health });
+ app.use(healthRouter);
app.use(router.handler());
app.use(middleware.notFound());
app.use(middleware.error());