NZ Ferns Library

A reference library of roughly 380 New Zealand fern species. You can search and filter the whole collection, open any species for taxonomy, conservation status, imagery and a distribution map, or work through a dichotomous key to identify a fern by its traits.

Species detail page for Adiantum aethiopicum, showing quick facts, taxonomy and imagery
A species page: quick facts, taxonomy and imagery in the herbarium layout.

Tech Stack

  • ·Frontend: React 19 with TypeScript, Vite (SWC), and Tailwind v4, with React Router 7 across six pages.
  • ·Backend: Express 5 on Node, MongoDB through Mongoose. Large GeoJSON region data is stored in GridFS rather than as a regular document, and responses are gzipped with the compression middleware.

How It Works

The data came from merging two sources: a 2025 taxonomy dataset that's strong on scientific structure (family, order, class, authorship) but thin on description, and an older dataset rich in narrative (common names, conservation status, distribution, habitat, recognition). I merge them by exact scientific-name match and overlay the narrative fields where a species exists in both. A seed script then enriches each record with images pulled from the Wikimedia Commons API before writing everything into MongoDB.

The frontend loads the full collection once and does all the searching, filtering and sorting in the browser. At ~380 species that's completely fine, and it keeps the interaction instant. Search runs across scientific names, common names, family, genus, synonyms and biostatus, with filters for family and conservation tier, an endemic-only toggle, sorting, and a CSV export of whatever's currently filtered. The list API is the one choke point if it ever needs pagination, and I know exactly where that is.

The fern index: a searchable, filterable table of species with family and conservation status
The full collection in one searchable table, with family and status filters, an endemic-only toggle and CSV export, all running client-side.

There's also an interactive dichotomous key for identifying a fern from scratch. Each step asks a single trait question (with a definition and a reference photo), and answering narrows a live candidate list while keeping a decision path you can step back through at any point. It runs over the same in-memory dataset, so the candidates recalculate instantly with every choice.

The interactive dichotomous key, asking a trait question with a live candidate list
The dichotomous key: each answer narrows the 273 candidates, with a decision path you can step back through.

The distribution map is the part I'm most pleased with. Each species has a free-text distribution string like 'Northland to Auckland' or 'throughout the South Island', and I parse that into actual regions. There's alias handling for spelling variants (Hawke's Bay, Hawkes Bay, Hawke s Bay all resolve to the same region), range connectors like 'to' and 'through' that expand to every region in between, and nationwide hints that fill in all 16. The matched regions then render as an SVG built from the GeoJSON, with custom projection maths to fit it into the viewBox. If the distribution text is missing, it greys out and shows a 'No data' badge rather than guessing.

Distribution map for Adiantum aethiopicum, with recorded regions shaded green
Distribution parsed straight from a free-text range string into shaded regions.

Because the two datasets don't perfectly overlap, the UI is built to tolerate gaps everywhere: fallback accessors check the new field then the old one, missing facts render as a dash, and unknown endemicity says 'unknown' instead of breaking. The regions endpoint also does proper HTTP caching (ETag, Last-Modified, 304s, a one-day max-age) since that GeoJSON file is large and rarely changes. The styling leans into a herbarium aesthetic: cream paper background, a single fern-green signal colour, hairline borders, no shadows.

Decisions

  • ·Merge taxonomy + narrative datasets, tolerate the gaps: the fallback-field pattern lets the two schemas coexist without breaking the UI.
  • ·GridFS for the regions GeoJSON: streams instead of loading into memory, and dodges the BSON size limit.
  • ·Parse free-text distribution into a real map: the alias and range-expansion logic turns messy strings into something visual.