Skip to content

Commit 18f2551

Browse files
feat(revalidate): add type validation and README to revalidate endpoint
Agent-Logs-Url: https://github.com/MobilityData/mobilitydatabase-web/sessions/90eff8fb-c818-4fa7-8edc-49790020ce49 Co-authored-by: Alessandro100 <18631060+Alessandro100@users.noreply.github.com>
1 parent 655a8d7 commit 18f2551

3 files changed

Lines changed: 131 additions & 1 deletion

File tree

src/app/api/revalidate/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# `/api/revalidate` Endpoint
2+
3+
This endpoint is used by external systems to trigger on-demand Next.js cache revalidation for feed pages. It exposes two HTTP methods:
4+
5+
- **GET** — Used exclusively by the Vercel cron job to revalidate all GBFS feed pages on a schedule.
6+
- **POST** — Used by external systems to trigger targeted or full-site cache revalidation.
7+
8+
---
9+
10+
## GET
11+
12+
Revalidates all GBFS feed pages. This handler is invoked automatically by Vercel's cron scheduler (configured in `vercel.json`) at 4am UTC Monday–Saturday and 7am UTC Sunday.
13+
14+
### Authentication
15+
16+
Vercel automatically passes an `Authorization: Bearer <CRON_SECRET>` header on every cron invocation. The value must match the `CRON_SECRET` environment variable.
17+
18+
### Response
19+
20+
| Status | Body |
21+
|--------|------|
22+
| `200` | `{ "ok": true, "message": "All GBFS feeds revalidated successfully" }` |
23+
| `401` | `{ "ok": false, "error": "Unauthorized" }` |
24+
| `500` | `{ "ok": false, "error": "Server misconfigured: CRON_SECRET missing" }` |
25+
| `500` | `{ "ok": false, "error": "Revalidation failed" }` |
26+
27+
---
28+
29+
## POST
30+
31+
Triggers targeted cache revalidation. The caller controls the scope of revalidation via the request body.
32+
33+
### Authentication
34+
35+
Include the `x-revalidate-secret` header with the value of the `REVALIDATE_SECRET` environment variable.
36+
37+
```
38+
x-revalidate-secret: <REVALIDATE_SECRET>
39+
```
40+
41+
### Request Body
42+
43+
```json
44+
{
45+
"type": "<revalidation-type>",
46+
"feedIds": ["<feed-id-1>", "<feed-id-2>"]
47+
}
48+
```
49+
50+
| Field | Type | Required | Description |
51+
|-------|------|----------|-------------|
52+
| `type` | `string` | Yes | The revalidation scope. Must be one of the valid types listed below. |
53+
| `feedIds` | `string[]` | Only for `specific-feeds` | List of feed IDs to revalidate. Ignored for all other types. |
54+
55+
If the request body is missing or cannot be parsed, the endpoint falls back to the default: `type: "specific-feeds"` with an empty `feedIds` array (no-op).
56+
57+
### Valid `type` Values
58+
59+
| Type | Description |
60+
|------|-------------|
61+
| `full` | Revalidates the entire site (all pages and all cache tags). |
62+
| `all-feeds` | Revalidates all GTFS, GTFS-RT, and GBFS feed detail pages and their shared cache tags. |
63+
| `all-gtfs-feeds` | Revalidates all GTFS feed detail pages and the `feed-type-gtfs` cache tag. |
64+
| `all-gtfs-rt-feeds` | Revalidates all GTFS-RT feed detail pages and the `feed-type-gtfs_rt` cache tag. |
65+
| `all-gbfs-feeds` | Revalidates all GBFS feed detail pages and the `feed-type-gbfs` cache tag. |
66+
| `specific-feeds` | Revalidates only the pages for the feed IDs listed in `feedIds`. Each feed is revalidated across all feed-type paths (GTFS, GTFS-RT, GBFS) and all locales. |
67+
68+
If an unrecognized or missing `type` is provided, the endpoint returns a `500` error:
69+
70+
```json
71+
{ "ok": false, "error": "invalid or missing type parameter" }
72+
```
73+
74+
### Response
75+
76+
| Status | Body |
77+
|--------|------|
78+
| `200` | `{ "ok": true, "message": "Revalidation triggered successfully" }` |
79+
| `401` | `{ "ok": false, "error": "Unauthorized" }` |
80+
| `500` | `{ "ok": false, "error": "Server misconfigured: REVALIDATE_SECRET missing" }` |
81+
| `500` | `{ "ok": false, "error": "invalid or missing type parameter" }` |
82+
| `500` | `{ "ok": false, "error": "Failed to revalidate" }` |
83+
84+
### Example Request
85+
86+
```bash
87+
curl -X POST https://mobilitydatabase.org/api/revalidate \
88+
-H "Content-Type: application/json" \
89+
-H "x-revalidate-secret: <REVALIDATE_SECRET>" \
90+
-d '{ "type": "specific-feeds", "feedIds": ["feed-abc123", "feed-def456"] }'
91+
```

src/app/api/revalidate/route.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,8 +449,31 @@ describe('POST /api/revalidate', () => {
449449
expect(mockRevalidatePath).toHaveBeenCalledTimes(36);
450450
});
451451

452-
it('handles specific-feeds with empty feedIds', async () => {
452+
it('returns 500 when type is invalid', async () => {
453453
const request = new Request('http://localhost:3000/api/revalidate', {
454+
method: 'POST',
455+
headers: {
456+
'x-revalidate-secret': 'test-secret',
457+
'content-type': 'application/json',
458+
},
459+
body: JSON.stringify({
460+
type: 'not-a-valid-type',
461+
}),
462+
});
463+
464+
const response = await POST(request);
465+
const json = await response.json();
466+
467+
expect(response.status).toBe(500);
468+
expect(json).toEqual({
469+
ok: false,
470+
error: 'invalid or missing type parameter',
471+
});
472+
expect(mockRevalidatePath).not.toHaveBeenCalled();
473+
expect(mockRevalidateTag).not.toHaveBeenCalled();
474+
});
475+
476+
it('handles specific-feeds with empty feedIds', async () => { const request = new Request('http://localhost:3000/api/revalidate', {
454477
method: 'POST',
455478
headers: {
456479
'x-revalidate-secret': 'test-secret',

src/app/api/revalidate/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ type RevalidateTypes =
1717
| 'all-gtfs-feeds'
1818
| 'specific-feeds';
1919

20+
const VALID_REVALIDATE_TYPES: RevalidateTypes[] = [
21+
'full',
22+
'all-feeds',
23+
'all-gbfs-feeds',
24+
'all-gtfs-rt-feeds',
25+
'all-gtfs-feeds',
26+
'specific-feeds',
27+
];
28+
2029
interface RevalidateBody {
2130
feedIds: string[]; // only for 'specific-feeds' revalidation type
2231
type: RevalidateTypes;
@@ -104,6 +113,13 @@ export async function POST(req: Request): Promise<NextResponse> {
104113
payload = { ...defaultRevalidateOptions };
105114
}
106115

116+
if (!VALID_REVALIDATE_TYPES.includes(payload.type)) {
117+
return NextResponse.json(
118+
{ ok: false, error: 'invalid or missing type parameter' },
119+
{ status: 500 },
120+
);
121+
}
122+
107123
// NOTE
108124
// revalidatePath = triggers revalidation for entire page cache
109125
// revalidateTag = triggers revalidation for API calls using `unstable_cache` with matching tags (e.g., feed-123, guest-feeds)

0 commit comments

Comments
 (0)