Skip to content

Commit 88fc84b

Browse files
committed
feat: add request body schema version header support
1 parent 59d94a3 commit 88fc84b

8 files changed

Lines changed: 131 additions & 1 deletion

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ PAYSTACK_PUBLIC_KEY=
2424
# API Configuration
2525
API_VERSION=1.0.0
2626
ENABLE_API_VERSION_HEADER=true
27+
ENABLE_SCHEMA_VERSION_HEADER=true
2728
ENABLE_RESPONSE_TIMING=true
2829
ENABLE_REQUEST_LOGGING=true

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The server is responsible for:
1515
- notifications, analytics, and moderation workflows
1616
- access checks for gated off-chain content
1717

18-
See [Backend Domain Model and Endpoint Boundaries](./docs/architecture/domain-boundaries.md) for a technical overview.
18+
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.
1919

2020
## Tech
2121

docs/api-versioning.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# API and Schema Versioning
2+
3+
The Access Layer Server uses versioning headers to inform clients about the current API version and the expected structure of request bodies.
4+
5+
## Response Headers
6+
7+
### `X-API-Version`
8+
Indicates the current overall version of the API. This is typically used for tracking feature sets and major API releases.
9+
10+
### `X-Schema-Version`
11+
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.
12+
13+
## Versioning Strategy
14+
15+
Both headers follow [Semantic Versioning (SemVer)](https://semver.org/):
16+
- **MAJOR** version: Breaking changes to the API or schema.
17+
- **MINOR** version: Backwards-compatible new features or additions.
18+
- **PATCH** version: Backwards-compatible bug fixes.
19+
20+
## Expected Consumer Behavior
21+
22+
1. **Check Headers**: Consumers should inspect the `X-Schema-Version` header in API responses.
23+
2. **Schema Alignment**: If the `X-Schema-Version` major version changes, consumers must update their request body structures to match the new schema requirements.
24+
3. **Warning Handling**: Consumers may choose to log warnings if they detect a version mismatch that they haven't yet updated to support.

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { appRateLimit } from './middlewares/rate.middleware';
1212
import { requestIdMiddleware } from './middlewares/request-id.middleware';
1313
import { responseTimingMiddleware } from './middlewares/response-timing.middleware';
1414
import { apiVersionMiddleware } from './middlewares/api-version.middleware';
15+
import { schemaVersionMiddleware } from './middlewares/schema-version.middleware';
1516
import { requestLoggerMiddleware } from './middlewares/request-logger.middleware';
1617
import { envConfig } from './config';
1718

@@ -21,6 +22,7 @@ const app: Express = express();
2122
app.set('trust proxy', 1);
2223
app.use(responseTimingMiddleware);
2324
app.use(apiVersionMiddleware);
25+
app.use(schemaVersionMiddleware);
2426
app.use(requestIdMiddleware);
2527
app.use(corsMiddleware());
2628
app.use(helmet());

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const envSchema = z.object({
5252
ENABLE_RESPONSE_TIMING: z.coerce.boolean().default(true),
5353
API_VERSION: z.string().default('1.0.0'),
5454
ENABLE_API_VERSION_HEADER: z.coerce.boolean().default(true),
55+
ENABLE_SCHEMA_VERSION_HEADER: z.coerce.boolean().default(true),
5556
ENABLE_REQUEST_LOGGING: z.coerce.boolean().default(true),
5657
INDEXER_JITTER_FACTOR: z.coerce.number().min(0).max(1).default(0.1),
5758
});

src/constants/schema.constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// src/constants/schema.constants.ts
2+
3+
/**
4+
* Current version of the request body schema.
5+
* This version should be bumped whenever there are breaking changes to the request body structure.
6+
*/
7+
export const REQUEST_SCHEMA_VERSION = '1.0.0';
8+
9+
/**
10+
* The response header key that carries the active request schema version.
11+
*/
12+
export const SCHEMA_VERSION_HEADER = 'X-Schema-Version';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { strict as assert } from 'assert';
2+
import { schemaVersionMiddleware } from './schema-version.middleware';
3+
import type { Request, Response, NextFunction } from 'express';
4+
import { REQUEST_SCHEMA_VERSION, SCHEMA_VERSION_HEADER } from '../constants/schema.constants';
5+
import { envConfig } from '../config';
6+
7+
// Minimal mock helpers
8+
function mockRes() {
9+
const headers: Record<string, string> = {};
10+
return {
11+
headers,
12+
setHeader(name: string, value: string) {
13+
headers[name] = value;
14+
},
15+
} as unknown as Response & { headers: Record<string, string> };
16+
}
17+
18+
function mockReq() {
19+
return {} as Request;
20+
}
21+
22+
function run() {
23+
// sets schema version header when enabled
24+
{
25+
const res = mockRes();
26+
let called = false;
27+
const next: NextFunction = () => {
28+
called = true;
29+
};
30+
31+
// Ensure it's enabled for the test
32+
const originalValue = envConfig.ENABLE_SCHEMA_VERSION_HEADER;
33+
(envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = true;
34+
35+
schemaVersionMiddleware(mockReq(), res, next);
36+
37+
assert.equal(res.headers[SCHEMA_VERSION_HEADER], REQUEST_SCHEMA_VERSION);
38+
assert.ok(called, 'next() should be called');
39+
40+
// Restore
41+
(envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = originalValue;
42+
}
43+
44+
// does not set header when disabled
45+
{
46+
const res = mockRes();
47+
let called = false;
48+
const next: NextFunction = () => {
49+
called = true;
50+
};
51+
52+
// Ensure it's disabled for the test
53+
const originalValue = envConfig.ENABLE_SCHEMA_VERSION_HEADER;
54+
(envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = false;
55+
56+
schemaVersionMiddleware(mockReq(), res, next);
57+
58+
assert.ok(!(SCHEMA_VERSION_HEADER in res.headers), 'Header should not be set');
59+
assert.ok(called, 'next() should be called');
60+
61+
// Restore
62+
(envConfig as any).ENABLE_SCHEMA_VERSION_HEADER = originalValue;
63+
}
64+
65+
console.log('schema-version.middleware tests passed');
66+
}
67+
68+
run();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// src/middlewares/schema-version.middleware.ts
2+
import { Request, Response, NextFunction } from 'express';
3+
import { envConfig } from '../config';
4+
import { REQUEST_SCHEMA_VERSION, SCHEMA_VERSION_HEADER } from '../constants/schema.constants';
5+
6+
/**
7+
* Middleware that adds a schema version header to the response.
8+
*
9+
* This header informs the client about the expected structure of request bodies.
10+
*
11+
* Can be enabled/disabled via the `ENABLE_SCHEMA_VERSION_HEADER` environment variable.
12+
*/
13+
export const schemaVersionMiddleware = (
14+
_req: Request,
15+
res: Response,
16+
next: NextFunction
17+
): void => {
18+
if (envConfig.ENABLE_SCHEMA_VERSION_HEADER) {
19+
res.setHeader(SCHEMA_VERSION_HEADER, REQUEST_SCHEMA_VERSION);
20+
}
21+
next();
22+
};

0 commit comments

Comments
 (0)