The first version of BosniaAir had two parts. A Next.js frontend, and a separate API written in .NET 8 with ASP.NET Core and Entity Framework Core. A background scheduler pulled readings from the WAQI air quality API every 10 minutes, wrote them to a database, and the frontend asked my API for the latest row.
That worked until the backend host disappeared and the site started showing no data.
What broke
The frontend was up. The API was not. The host I had deployed the backend to now answered "Application not found". The service was gone, so every request from the frontend failed, and the page had nothing to draw.
I could have found another host for the .NET app. I looked at what the backend actually did first, and the answer was not much. It fetched station data from WAQI, mapped it to my JSON shape, stored it so I would not hit WAQI on every page view, and served it. It was a thin cache with a scheduler attached.
A cache like that does not need a process of its own, a database or a scheduler. It needs somewhere to keep a response for a few minutes.
What I did instead
I moved the API into the Next.js app as route handlers, so the frontend and the API are one deployment on Vercel. The paths are the same, /api/v1/air-quality/{city}/live, /forecast and /complete, and so is the JSON shape. The frontend components did not need to change, apart from calling the API on the same origin.
The scheduler and the database became the Next.js Data Cache:
const fetchCityStations = unstable_cache(fetchCityStationsUncached, ['waqi-city-stations-v2'], {
revalidate: UPSTREAM_REVALIDATE_SECONDS, // 300
})WAQI is called at most a few times per city per five minutes, however many people open the page. If a call throws, the error is not cached, so the next request tries again.
On top of that, the responses go out with a CDN cache header, so most requests never reach my code:
'Cache-Control': 'public, max-age=0, s-maxage=120, stale-while-revalidate=600'I also opened CORS for GET, since the data is public and read-only and I would like other small sites to be able to use it.
The old .NET project is still in the repo, in a backend/ folder, for reference. The README says so.
What else broke on the way
Moving code surfaces things the old setup was hiding.
- A dead station. The Tuzla station I was using had stopped reporting in August. The old setup had no way to tell me. I moved Tuzla to another station.
- Timestamps. Timestamps were not real UTC before. Now they are ISO 8601 in UTC, and the panel formats them in Europe/Sarajevo.
- A 503 for a missing forecast. A city without a current forecast (Travnik) made the API answer 503. That was not an outage, there was simply no forecast. Now the endpoint answers 200 with an empty list and
available: false, and the interface says there is no forecast. - Connections from the new server. The first requests to WAQI from the new setup timed out, and that has its own post.
The forecast one is an easy mistake to make: using an error status for something that is a normal answer. No forecast is data. An unreachable upstream is an error. They should not look alike to the client.
What I would keep from this
I would not call the .NET backend a mistake, it did its job while it ran. The mistake was putting a separate service in front of something that behaves like a cache. A service you have to deploy, watch and pay attention to is a liability when the whole product is one page and a few numbers.
The test I use now when I am about to add a backend to a small project is whether anything in it has to remember something the upstream will not give me again. If not, it is a cache, and a cache can live next to the frontend.
I kept the JSON shape identical through the move. That decision paid for itself, because the frontend kept working while the code under it changed completely.