Blocked by #195.
A public JSON endpoint for the competitions dataset, mirroring the one places already has.
This is close to free and it is the part of this vertical that is genuinely hard to copy. Anyone can list competitions on a web page; a schema-validated, machine-readable dataset of national qualifier pathways across 13 countries does not exist anywhere else. Publishing it makes the data addressable by other tools, by scripts, and by coding agents, which is a distribution channel nobody in this category is using.
Files
src/app/api/competitions/route.ts - new
src/lib/competitions-api.ts - new, query parsing and validation
src/app/api/competitions/route.test.ts - new
Copy the places API exactly
src/app/api/places/route.ts (43 lines) is the template:
:11 export const revalidate = 21600 (6 hours)
:14-18 wide-open CORS
:20-23 Cache-Control: public, max-age=3600, s-maxage=21600, stale-while-revalidate=86400
src/lib/places-api.ts (149 lines) is the query layer:
:13-16 PLACES_API_LIMITS { defaultLimit: 100, maxLimit: 500 }
:51-125 parsePlacesQuery, which rejects repeated params and unknown categories, and at :99-104 rejects a country filter because the places schema has no country field
:131-149 queryPlaces
Match those conventions rather than inventing new ones. An API that behaves differently from the sibling endpoint on the same site is worse than no second endpoint.
Query parameters
category, format, participation, region, country (matches country_tracks[].country), fee (free), age, deadline_before, limit, offset.
Unlike places, country is a valid filter here, since country_tracks exists. Note that difference explicitly in the code so the next reader does not assume it was copied by mistake.
Documentation
src/app/docs/(article)/places-api/page.tsx (210 lines) documents the places endpoint. Write the equivalent for competitions, or extend that page to cover both. Include a worked curl example and the response shape.
Acceptance criteria
Blocked by #195.
A public JSON endpoint for the competitions dataset, mirroring the one places already has.
This is close to free and it is the part of this vertical that is genuinely hard to copy. Anyone can list competitions on a web page; a schema-validated, machine-readable dataset of national qualifier pathways across 13 countries does not exist anywhere else. Publishing it makes the data addressable by other tools, by scripts, and by coding agents, which is a distribution channel nobody in this category is using.
Files
src/app/api/competitions/route.ts- newsrc/lib/competitions-api.ts- new, query parsing and validationsrc/app/api/competitions/route.test.ts- newCopy the places API exactly
src/app/api/places/route.ts(43 lines) is the template::11export const revalidate = 21600(6 hours):14-18wide-open CORS:20-23Cache-Control: public, max-age=3600, s-maxage=21600, stale-while-revalidate=86400src/lib/places-api.ts(149 lines) is the query layer::13-16PLACES_API_LIMITS { defaultLimit: 100, maxLimit: 500 }:51-125parsePlacesQuery, which rejects repeated params and unknown categories, and at:99-104rejects acountryfilter because the places schema has no country field:131-149queryPlacesMatch those conventions rather than inventing new ones. An API that behaves differently from the sibling endpoint on the same site is worse than no second endpoint.
Query parameters
category,format,participation,region,country(matchescountry_tracks[].country),fee(free),age,deadline_before,limit,offset.Unlike places,
countryis a valid filter here, sincecountry_tracksexists. Note that difference explicitly in the code so the next reader does not assume it was copied by mistake.Documentation
src/app/docs/(article)/places-api/page.tsx(210 lines) documents the places endpoint. Write the equivalent for competitions, or extend that page to cover both. Include a workedcurlexample and the response shape.Acceptance criteria
GET /api/competitionsreturns all records with correct CORS and cache headerslimitis capped at the maximumoffsetis stablesrc/app/api/competitions/route.test.ts, followingsrc/app/api/places/route.test.ts