Building A Photo Blog on Strapi Gatsby And Typescript: Part 2 Connecting the Backend to iNaturalist

Taylor Nodell
4 min readNov 18, 2020

Now that I’ve gotten my photo blog project set up in part 1, I can start building my Strapi backend.

The first thing is to create a new Collection content-type within Strapi. For each Photo, I know I’ll need a title, image, content, and tags for grouping photos in ways that iNaturalist data might not have. Each photo will also need an iNaturalist ID to make the api call with, and a JSON object to store the data that is returned. If I wanted, I could give all of the JSON response data their own fields, but for now this works and keeping iNat data together makes sense too.

Photo Content Type in Strapi
Photo Content-type in Strapi’s backend

Now to actually write some code! Strapi has the concept of a model, which are two files that describe the shape of the data and the options provided to that data. I’ve already described the shape of the data in the content-type builder and now I’ll add some lifecycle hooks to modify that data. AKA I’m going to make an a call to the iNaturalist API to populate the Photo model with additional data.

Creating a photo entry with Shingleback lizard
Photo entry

Lifecycle hooks are functions that get triggered when the Strapi queries are called.” Simple enough, so I jump into ./api/photo/models/photo.js and add a lifecyles object to the module.exports. I’ll use the beforeCreate lifecycle to add the additional iNaturalist data before the entry is created.

const fetch = require("cross-fetch");module.exports = {
lifecycles: {
async beforeCreate(input) {
const res = await fetch(
`https://api.inaturalist.org/v1/observations/${input.iNatID}`
);
if (res.status >= 400) {
throw new Error("Bad response from iNaturalist observations server");
}
const d = await res.json();
const iNatResponse = d.results[0];
}

async beforeCreate(input) input is the data that I’ve already entered from Strapi. From that, I want the iNatID to be able to make a call to the iNaturalist API. I’ve imported fetch to make that happen, and since we need that response back before we can do anything else, I’m using async/await. That response has a long list of data about the observation I made. I’m grabbing things like location data, data the photo was taken, and species name. One thing that the response doesn’t have is the taxonomy list for the species. Using the above Shingle Back Lizard as an example, I want “Lizards -> Skinks -> Social Skinks -> Blue-tongued Skinks -> Shingleback Lizard” and the corresponding scientific names.

To get that requires a second iNaturalist API call to their taxa endpoint: https://api.inaturalist.org/v1/taxa/${iNatResponse.taxon.id}

From that response, I’m building an array of objects with the rank (Domain, Kingdom, Phylum etc), scientific name, and common name:

iNatData.taxonAncestors = iNatTaxonResponse.ancestors.map((ancestor) => {
return {
rank: ancestor.rank,
name: ancestor.name,
preferredCommonName: ancestor.preferred_common_name
? ancestor.preferred_common_name
: null,
};
});

Send that back to Strapi by mutating the input parameter, and you’re good to go. input.iNatData = iNatData;

My backend now automatically updates with iNaturalist data when I create a new Photo entry, and I can access that from the front end to create some cool components and ways of sorting my photos!

I spent some time pulling out the old unused Article and Category code from the starting tutorial, and created a new Content-type for Tag. With that all in place, I’m ready to create the front end of my app.

Working with Strapi has been pretty seamless after updating everything and the iNaturalist API is a great resource as well. Here’s that Shingleback for following along.

Shingleback Lizard in Mungo, NSW

Github Repo and full Photo model code:

"use strict";
const fetch = require("cross-fetch");
/**
* Lifecycle callbacks for the `photo` model.
**/
module.exports = {
lifecycles: {
async beforeCreate(input) {
const res = await fetch(
`https://api.inaturalist.org/v1/observations/${input.iNatID}`
);
if (res.status >= 400) {
throw new Error("Bad response from iNaturalist observations server");
}
const d = await res.json();
const iNatResponse = d.results[0];
// hold all the iNatData in one json
let iNatData = {};
iNatData.commonName = iNatResponse.species_guess; //required
iNatData.placeGuess = iNatResponse.place_guess; // required
iNatData.dateTaken = iNatResponse.observed_on_details.date; // required
iNatData.latinName = iNatResponse.taxon.name; //required
iNatData.qualityGrade = iNatResponse.quality_grade;
iNatData.iNatDescription = iNatResponse.description;
iNatData.endemic = iNatResponse.taxon.endemic;
iNatData.threatened = iNatResponse.taxon.threatened;
iNatData.introduced = iNatResponse.taxon.introduced;
iNatData.native = iNatResponse.taxon.native;
// make a second call to iNat API to get taxon info
const taxonRes = await fetch(
`https://api.inaturalist.org/v1/taxa/${iNatResponse.taxon.id}`
);
if (taxonRes.status >= 400) {
throw new Error("Bad response from iNaturalist taxon server");
}
const x = await taxonRes.json();
const iNatTaxonResponse = x.results[0];
iNatData.taxonAncestors = iNatTaxonResponse.ancestors.map((ancestor) => {
return {
rank: ancestor.rank,
name: ancestor.name,
preferredCommonName: ancestor.preferred_common_name
? ancestor.preferred_common_name
: null,
};
});
// send it to strapi
input.iNatData = iNatData;
console.log("*************************");
console.log("input.iNatData");
console.log("*************************");
console.log(input.iNatData);
},
},
};

--

--

Taylor Nodell

Developer. Musician. Naturalist. Traveler. In any order. @tayloredtotaylor