Skip to content

Commit 7bcc0e1

Browse files
Merge pull request #109 from MobilityData/copilot/add-readme-to-revalidate-endpoint
Add README and invalid type validation to /api/revalidate
2 parents 60bca82 + 5da0df9 commit 7bcc0e1

3 files changed

Lines changed: 132 additions & 7 deletions

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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,30 @@ describe('POST /api/revalidate', () => {
449449
expect(mockRevalidatePath).toHaveBeenCalledTimes(36);
450450
});
451451

452+
it('returns 500 when type is invalid', async () => {
453+
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+
452476
it('handles specific-feeds with empty feedIds', async () => {
453477
const request = new Request('http://localhost:3000/api/revalidate', {
454478
method: 'POST',

src/app/api/revalidate/route.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ import {
99
revalidateSpecificFeeds,
1010
} from '../../utils/revalidate-feeds';
1111

12-
type RevalidateTypes =
13-
| 'full'
14-
| 'all-feeds'
15-
| 'all-gbfs-feeds'
16-
| 'all-gtfs-rt-feeds'
17-
| 'all-gtfs-feeds'
18-
| 'specific-feeds';
12+
const VALID_REVALIDATE_TYPES = [
13+
'full',
14+
'all-feeds',
15+
'all-gbfs-feeds',
16+
'all-gtfs-rt-feeds',
17+
'all-gtfs-feeds',
18+
'specific-feeds',
19+
] as const;
20+
21+
type RevalidateTypes = (typeof VALID_REVALIDATE_TYPES)[number];
1922

2023
interface RevalidateBody {
2124
feedIds: string[]; // only for 'specific-feeds' revalidation type
@@ -104,6 +107,13 @@ export async function POST(req: Request): Promise<NextResponse> {
104107
payload = { ...defaultRevalidateOptions };
105108
}
106109

110+
if (!VALID_REVALIDATE_TYPES.includes(payload.type)) {
111+
return NextResponse.json(
112+
{ ok: false, error: 'invalid or missing type parameter' },
113+
{ status: 500 },
114+
);
115+
}
116+
107117
// NOTE
108118
// revalidatePath = triggers revalidation for entire page cache
109119
// revalidateTag = triggers revalidation for API calls using `unstable_cache` with matching tags (e.g., feed-123, guest-feeds)

0 commit comments

Comments
 (0)