Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as postcodes from "../app/controllers/postcodes_controller";
import * as scottishPostcodes from "../app/controllers/scottish_postcodes_controller";
import * as terminatedPostcodes from "../app/controllers/terminated_postcodes_controller";
import { getConfig } from "./config";
import { apiCatalog, homepageLink } from "./well_known";

export const routes = (app: Express): void => {
const router = express.Router();
Expand Down Expand Up @@ -39,6 +40,8 @@ export const routes = (app: Express): void => {

const docsBuildPath = join(__dirname, "../../build");

router.get("/.well-known/api-catalog", apiCatalog);
router.get("/", homepageLink);
router.use(express.static(docsBuildPath));

const { urlPrefix } = getConfig();
Expand Down
55 changes: 55 additions & 0 deletions api/config/well_known.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { RequestHandler } from "express";

// RFC 9727 API catalog and RFC 8288 homepage Link header. Served on both
// postcodes.io and api.postcodes.io (same app).

export const API_CATALOG_PATH = "/.well-known/api-catalog";

export const catalog = {
linkset: [
{
anchor: "https://api.postcodes.io",
"service-desc": [
{
href: "https://api.postcodes.io/openapi.json",
type: "application/json",
},
],
"service-doc": [
{ href: "https://postcodes.io/docs/api", type: "text/html" },
],
status: [
{ href: "https://api.postcodes.io/ready", type: "application/json" },
],
},
],
};

const CATALOG_BODY = Buffer.from(JSON.stringify(catalog, null, 2));

export const CATALOG_CONTENT_TYPE =
'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"';

export const HOMEPAGE_LINK = [
`<${API_CATALOG_PATH}>; rel="api-catalog"`,
'<https://api.postcodes.io/openapi.json>; rel="service-desc"; type="application/json"',
'<https://postcodes.io/docs/api>; rel="service-doc"; type="text/html"',
].join(", ");

// Express answers HEAD with this handler too, keeping the Link header on HEAD.
// A Buffer body keeps the Content-Type exact: res.json would replace it and a
// string body gets "; charset=utf-8" appended.
export const apiCatalog: RequestHandler = (_, response) => {
response
.set({
"Content-Type": CATALOG_CONTENT_TYPE,
Link: `<${API_CATALOG_PATH}>; rel="api-catalog"`,
"Cache-Control": "public, max-age=3600",
})
.send(CATALOG_BODY);
};

export const homepageLink: RequestHandler = (_, response, next) => {
response.set("Link", HOMEPAGE_LINK);
next();
};
48 changes: 48 additions & 0 deletions test/well_known.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import request from "supertest";
import { postcodesioApplication } from "./helper";
import { CATALOG_CONTENT_TYPE } from "../api/config/well_known";

const app = postcodesioApplication();

describe("/.well-known/api-catalog", () => {
it("serves an RFC 9727 linkset", async () => {
const { headers, text } = await request(app)
.get("/.well-known/api-catalog")
.set("Accept", "application/linkset+json, application/json")
.expect(200);
expect(headers["content-type"]).toBe(CATALOG_CONTENT_TYPE);
expect(headers["link"]).toBe(
'</.well-known/api-catalog>; rel="api-catalog"'
);
expect(headers["access-control-allow-origin"]).toBe("*");
const [entry] = JSON.parse(text).linkset;
expect(entry.anchor).toBe("https://api.postcodes.io");
for (const rel of ["service-desc", "service-doc", "status"]) {
expect(entry[rel][0].href).toMatch(/^https:\/\//);
}
});

it("carries the api-catalog Link on HEAD", async () => {
const { headers } = await request(app)
.head("/.well-known/api-catalog")
.expect(200);
expect(headers["link"]).toBe(
'</.well-known/api-catalog>; rel="api-catalog"'
);
});
});

describe("homepage", () => {
it("carries a Link header to the api-catalog, spec and docs", async () => {
const { headers } = await request(app).get("/");
const link = headers["link"];
expect(link).toContain('</.well-known/api-catalog>; rel="api-catalog"');
expect(link).toContain(
'<https://api.postcodes.io/openapi.json>; rel="service-desc"'
);
expect(link).toContain(
'<https://postcodes.io/docs/api>; rel="service-doc"'
);
});
});
Loading