The map is the easy part: what building a geospatial platform really taught me
By Bahar Arabzadeh, Frontend Developer at Bitagreen
When I joined Bitagreen, I assumed building map interfaces would be a variation of what I already knew — components, state, events. I was wrong in the best possible way.
Bitagreen builds geospatial platforms for climate resilience and vegetation management: Altamira for climate risk and nature-based adaptation, and Vista for predictive green infrastructure maintenance. We combine geospatial analysis, climate data, and scenario modeling to help urban planners, infrastructure teams, green space managers, and climate-risk specialists make decisions — where is risk highest, where should green infrastructure go, what interventions actually move the needle. For us, the map isn't decorative. It's where our users make decisions.
What I didn't expect was how much this work would push me into data modeling, architectural thinking, and some genuinely philosophical questions about tooling. Here's what I learned.
1. Know your options before you pick
There's no single "best" map library — and choosing the wrong one early is expensive.
Before writing a single line of code, I spent time understanding the tradeoff space:
Leaflet — simple, lightweight, great for markers and basic interactions. If you just need to drop pins on a map, this is your tool.
Mapbox GL / MapLibre GL — designed for data-heavy, high-performance visualizations using vector tiles. Styling is done via expressions, not CSS. This is where things get interesting.
Esri (ArcGIS) — the enterprise GIS ecosystem. Deep feature set, but a different mental model entirely.
We went with Mapbox GL because our use case is data-dense: multiple overlapping layers, continuous color scales, threshold-based styling, and real-time interaction. But there's another reason: Mapbox is also where we upload and host our map data. Having the data and the styling in the same ecosystem means the pipeline from raw geospatial data to a styled, interactive layer is much tighter. And the custom styling is genuinely powerful — the level of control you get over how data looks at different zoom levels, under different conditions, is hard to match. We also looked at MapLibre GL, the open-source fork of Mapbox GL, and weighed the trade-off of committing to one vendor. For us, the integrated hosting and styling tipped the balance. And choosing Mapbox for our own interface doesn't limit interoperability: our outputs are delivered in standard formats (from GeoJSON, Shapefile and WMS/WFS to cloud-native GeoParquet), so teams working in ArcGIS can use them directly.
The decision matters because it shapes everything downstream — how you store data, how you style it, what the developer experience looks like, and how well it scales. And it's not a frontend decision alone. Choosing Mapbox meant decisions on the backend too: how data gets processed and uploaded, whether you need a separate tile server, how updates flow from the data pipeline to what's visible on the map. It's one of those early choices where everyone at the table needs to understand the tradeoffs.
2. The real challenge is the data, not the map
This one took me a while to fully internalize.
The map is just the output. The hard decisions happen before you draw anything:
Interactive layer or backdrop? Is this a layer the user can click and filter, or purely visual context?
Vector or raster? Vector tiles are flexible and style-able at runtime. Raster tiles are pre-rendered and faster to serve, but frozen.
Categorical or continuous data? A layer showing land-use categories needs a different encoding than one showing temperature gradients.
Each of these answers changes how you store, serve, and style the data.
And the styling itself isn't CSS — it's Mapbox expressions. Instead of writing color: red you write something like:
["interpolate", ["linear"], ["get", "risk_score"], 0, "#00ff00", 50, "#ffff00", 100, "#ff0000"]
That expression is computed directly from the data, at render time, for every feature on the map. Once you start thinking that way — styling as a function of data — you stop treating the map like a UI component and start treating it like a data visualization pipeline.
One thing that's easy to overlook: ["get", "risk_score"] only works if the data actually has a field called risk_score. That means before writing a single line of styling, I need to know exactly what's in the data — what the property names are, what the value ranges look like, whether a field is always present or sometimes null. That's a conversation with the geospatial specialists who generate the maps — agreeing on field names, value ranges, and what's guaranteed to be present before a single line of styling gets written. Getting that contract wrong means the layer silently renders nothing, or worse, renders incorrectly. In geospatial work especially, the API contract isn't just about endpoints — it's about the shape of every feature in the file.
3. Architecture is what decides whether you scale
We have a lot of layers. We'll have more. The architecture had to support that without making every new layer a new engineering task.
The approach we landed on: layers as configuration, not code.
Instead of building a new component for each layer type, we built two generic rendering components — one for raster, one for vector. Each layer is returned from the backend as a config object — the frontend never hardcodes layer definitions. The rendering component just reads it:
// What the backend returns
{
name: "green_roofs",
type: "vector",
url: "<https://storage/.../green_roofs.geojson>",
mapbox_tileset_source_id: "884bebd6cd3fa6a224e58e289a936305",
metadata: {
layers: [
{
name: "green_roofs",
color: "#86B050",
categorical: false,
}
]
}
}
// What the frontend does with it — just renders
<VectorLayer id={map.name} config={map} />
For layers with continuous data, the same VectorLayer component automatically builds a Mapbox step expression from the config:
["step", ["get", valueIdentifier], ...stepsColors]
Same component, different config. Adding a new layer is just a new object from the backend — no new component, no new code path, no new PR.
This paid off in an unexpected way: it enabled our internal tooling. Because everything is config-driven, climate-risk specialists at Bitagreen can add layers, adjust color thresholds, and update legends themselves — through an internal platform — without any engineer in the loop. What would have been a back-and-forth ticket queue is now self-service.
That's the kind of architectural decision that looks small at first and turns out to matter a lot.
4. How AI changed the workflow
The honest answer: it removed a specific kind of friction that used to slow me down constantly.
As a frontend developer, one of the core skills is translating a design into working code — and this is probably the area most transformed by AI. I look at a design, I know what it should do, and I used to spend a lot of time on the gap in between: looking up property names, remembering the right combination, recalling whether it's align-items or justify-content this time. Small things, but they add up. With AI in the loop, that gap almost disappears. I'm not thinking about syntax anymore — I'm thinking about whether the output matches the intent. That shift sounds small but it meaningfully changes how fast a component comes together.
Mapbox has a large API surface — expressions, static images, tile endpoints, style specs, camera controls. As a frontend engineer integrating with it, you're constantly reaching for the documentation. AI compresses that significantly. Instead of reading through pages of docs to figure out what parameters to send for a static map thumbnail, or how a particular API works, I just describe what I need and get a working starting point. I still read the output carefully, but the time from "I need to figure out how this works" to "I have something running" is much shorter.
Where AI still falls short
From my experience, the area where AI still falls short is system design and architecture. Not because it can't write code — it can, and it produces working UI quickly. The problem is how it gets there. Every time you ask it to implement a new feature, it tends to overfit the architecture to that feature. It optimizes for visible results — the thing works, it looks right, there are no obvious bugs. But it does that at the expense of everything you've built before. Stability, consistency, maintainability — those aren't in the prompt.


Good architecture doesn't point everything at the current goal. It compromises. The result is never the most elegant solution to any single feature — it's the line in between, the one that keeps the whole codebase navigable. AI doesn't hold that line. It doesn't know your conventions, your history, or where the product is heading. Six months later, when you're adding something new, you feel the cost of every decision that overfitted to a previous feature.
That's the part that still needs a human in the room



Comments