diff --git a/.env.example b/.env.example index f396361..d62e993 100644 --- a/.env.example +++ b/.env.example @@ -24,5 +24,6 @@ PAYSTACK_PUBLIC_KEY= # API Configuration API_VERSION=1.0.0 ENABLE_API_VERSION_HEADER=true +ENABLE_SCHEMA_VERSION_HEADER=true ENABLE_RESPONSE_TIMING=true ENABLE_REQUEST_LOGGING=true diff --git a/README.md b/README.md index 0ddf516..538fb13 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The server is responsible for: - notifications, analytics, and moderation workflows - access checks for gated off-chain content -See [Backend Domain Model and Endpoint Boundaries](./docs/architecture/domain-boundaries.md) for a technical overview. +See [Backend Domain Model and Endpoint Boundaries](./docs/architecture/domain-boundaries.md) for a technical overview and [API Versioning](./docs/api-versioning.md) for details on schema versioning. ## Tech diff --git a/docs/api-versioning.md b/docs/api-versioning.md new file mode 100644 index 0000000..669ba3d --- /dev/null +++ b/docs/api-versioning.md @@ -0,0 +1,24 @@ +# API and Schema Versioning + +The Access Layer Server uses versioning headers to inform clients about the current API version and the expected structure of request bodies. + +## Response Headers + +### `X-API-Version` +Indicates the current overall version of the API. This is typically used for tracking feature sets and major API releases. + +### `X-Schema-Version` +Indicates the active version of the request body schema. This version should be checked by consumers to ensure they are sending request bodies in the format expected by the server. + +## Versioning Strategy + +Both headers follow [Semantic Versioning (SemVer)](https://semver.org/): +- **MAJOR** version: Breaking changes to the API or schema. +- **MINOR** version: Backwards-compatible new features or additions. +- **PATCH** version: Backwards-compatible bug fixes. + +## Expected Consumer Behavior + +1. **Check Headers**: Consumers should inspect the `X-Schema-Version` header in API responses. +2. **Schema Alignment**: If the `X-Schema-Version` major version changes, consumers must update their request body structures to match the new schema requirements. +3. **Warning Handling**: Consumers may choose to log warnings if they detect a version mismatch that they haven't yet updated to support. diff --git a/src/app.ts b/src/app.ts index 791481f..5bc8cb0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,6 +12,7 @@ import { appRateLimit } from './middlewares/rate.middleware'; import { requestIdMiddleware } from './middlewares/request-id.middleware'; import { responseTimingMiddleware } from './middlewares/response-timing.middleware'; import { apiVersionMiddleware } from './middlewares/api-version.middleware'; +import { schemaVersionMiddleware } from './middlewares/schema-version.middleware'; import { requestLoggerMiddleware } from './middlewares/request-logger.middleware'; import { envConfig } from './config'; @@ -21,6 +22,7 @@ const app: Express = express(); app.set('trust proxy', 1); app.use(responseTimingMiddleware); app.use(apiVersionMiddleware); +app.use(schemaVersionMiddleware); app.use(requestIdMiddleware); app.use(corsMiddleware()); app.use(helmet()); diff --git a/src/config.ts b/src/config.ts index 0baef39..1c5fc6a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -52,6 +52,7 @@ export const envSchema = z.object({ ENABLE_RESPONSE_TIMING: z.coerce.boolean().default(true), API_VERSION: z.string().default('1.0.0'), ENABLE_API_VERSION_HEADER: z.coerce.boolean().default(true), + ENABLE_SCHEMA_VERSION_HEADER: z.coerce.boolean().default(true), ENABLE_REQUEST_LOGGING: z.coerce.boolean().default(true), INDEXER_JITTER_FACTOR: z.coerce.number().min(0).max(1).default(0.1), }); diff --git a/src/constants/schema.constants.ts b/src/constants/schema.constants.ts new file mode 100644 index 0000000..430cd15 --- /dev/null +++ b/src/constants/schema.constants.ts @@ -0,0 +1,12 @@ +// src/constants/schema.constants.ts + +/** + * Current version of the request body schema. + * This version should be bumped whenever there are breaking changes to the request body structure. + */ +export const REQUEST_SCHEMA_VERSION = '1.0.0'; + +/** + * The response header key that carries the active request schema version. + */ +export const SCHEMA_VERSION_HEADER = 'X-Schema-Version'; diff --git a/src/middlewares/schema-version.middleware.test.ts b/src/middlewares/schema-version.middleware.test.ts new file mode 100644 index 0000000..a036fea --- /dev/null +++ b/src/middlewares/schema-version.middleware.test.ts @@ -0,0 +1,68 @@ +import { strict as assert } from 'assert'; +import { schemaVersionMiddleware } from './schema-version.middleware'; +import type { Request, Response, NextFunction } from 'express'; +import { REQUEST_SCHEMA_VERSION, SCHEMA_VERSION_HEADER } from '../constants/schema.constants'; +import { envConfig } from '../config'; + +// Minimal mock helpers +function mockRes() { + const headers: Record = {}; + return { + headers, + setHeader(name: string, value: string) { + headers[name] = value; + }, + } as unknown as Response & { headers: Record }; +} + +function mockReq() { + return {} as Request; +} + +function run() { + // sets schema version header when enabled + { + const res = mockRes(); + let called = false; + const next: NextFunction = () => { + called = true; + }; + + // Ensure it's enabled for the test + const originalValue = envConfig.ENABLE_SCHEMA_VERSION_HEADER; + (envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = true; + + schemaVersionMiddleware(mockReq(), res, next); + + assert.equal(res.headers[SCHEMA_VERSION_HEADER], REQUEST_SCHEMA_VERSION); + assert.ok(called, 'next() should be called'); + + // Restore + (envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = originalValue; + } + + // does not set header when disabled + { + const res = mockRes(); + let called = false; + const next: NextFunction = () => { + called = true; + }; + + // Ensure it's disabled for the test + const originalValue = envConfig.ENABLE_SCHEMA_VERSION_HEADER; + (envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = false; + + schemaVersionMiddleware(mockReq(), res, next); + + assert.ok(!(SCHEMA_VERSION_HEADER in res.headers), 'Header should not be set'); + assert.ok(called, 'next() should be called'); + + // Restore + (envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = originalValue; + } + + console.log('schema-version.middleware tests passed'); +} + +run(); diff --git a/src/middlewares/schema-version.middleware.ts b/src/middlewares/schema-version.middleware.ts new file mode 100644 index 0000000..64b63f4 --- /dev/null +++ b/src/middlewares/schema-version.middleware.ts @@ -0,0 +1,22 @@ +// src/middlewares/schema-version.middleware.ts +import { Request, Response, NextFunction } from 'express'; +import { envConfig } from '../config'; +import { REQUEST_SCHEMA_VERSION, SCHEMA_VERSION_HEADER } from '../constants/schema.constants'; + +/** + * Middleware that adds a schema version header to the response. + * + * This header informs the client about the expected structure of request bodies. + * + * Can be enabled/disabled via the `ENABLE_SCHEMA_VERSION_HEADER` environment variable. + */ +export const schemaVersionMiddleware = ( + _req: Request, + res: Response, + next: NextFunction +): void => { + if (envConfig.ENABLE_SCHEMA_VERSION_HEADER) { + res.setHeader(SCHEMA_VERSION_HEADER, REQUEST_SCHEMA_VERSION); + } + next(); +};