Best/canonical ways to interact with backend services?
I read a tweet today that said not to trust any advice unless it starts off with “it depends”.
So, it depends
Going to share how I think about interacting with other APIs – not best, not canonical, just how I think … and have started to shape this in my head from a RW perspective.
- Do I need to present the data back to the web in a cell?
For example, am I calling a CMS like Contentful or Sanity and want to render a list of the results?
Case in point, I made a Contentful space for my niece over a year ago where she can upload pictures of various "cupcake characters" she’s made out of some Disney Princess figurines/games – she’s 8. And she uses Siri and an iPad to take photos of them and dictate little descriptions.
I can use Contentful’s SDK to fetch the cupcakes in a RW service and the sdl so that I can use cells to render a list of cupcakes or show a single cupcake:
import { createClient } from 'contentful'
const client = createClient({
space: process.env.CONTENTFUL_SPACE,
accessToken: process.env.CONTENTFUL_DELIVERY_API_KEY,
})
const renderAsset = (fields) => {
return { title: fields.title, file: fields.file }
}
const renderAssets = (assets) => {
return assets.map((asset) => renderAsset(asset.fields))
}
const renderEntry = (entry) => {
return {
id: entry.sys.id,
name: entry.fields.name,
description: entry.fields.description,
price: entry.fields.price,
rating: entry.fields.rating,
slug: entry.fields.slug,
photos: renderAssets(entry.fields.photos),
}
}
export const cupcakes = async () => {
const response = await client.getEntries({
content_type: 'cupcake',
limit: 1000,
order: 'fields.name',
})
return response.items.map((entry) => renderEntry(entry))
}
export const cupcake = async ({ id }) => {
const entry = await client.getEntry(id)
return renderEntry(entry)
}
and SDL
import gql from 'graphql-tag'
export const schema = gql`
type Cupcake {
id: String!
name: String!
description: String!
price: Float
rating: Int
slug: String!
photos: [ContentfulAsset]
}
type Query {
cupcakes: [Cupcake!]!
cupcake(id: String!): Cupcake!
}
`
RW is none the wiser that my Cupcakes didn’t come from Prisma/SQL database, but came from the Contentful API call.
- I don’t need to render anything, the API I’m interacting with maybe posts some request or or returns some other response
Here, I’m thinking about an API like Mailgun.
In this case, I’d probably use a function vs a service – or probable a function that calls a service like MailManager
.
The Mailgun API endpoints I might call are:
I wouldn’t make a SDL for this or want to interact with the API via gql, I don’t think.
I do have some ideas for some best practices around functions and this is where I really like your thinking:
i could imagine a generator based on an OpenAPI spec (though that’s not universally adopted)? or perhaps just to reduce some of the boilerplate
For me, there are things I do in functions over and over when calling a 3rd party API:
- security – check that whoever is calling the function is allowed to call it
- http status codes. handle 200 success. handle 500 error. 401 unauthorized 400 bad request, 403 forbidden. 204 no content, etc.
- parse/process request
- return result
If you think about it, it’s no so unlike cells with “loading” “error” “empty” and “success” cases.
So, to that end, could there be something imported into a function that establishes those states and handles in the response is empty → 204 or unauthorized → 401 or not permitted → 403 “for you”?
Also, 7 times out of 10 if you are making a a call to another API, you might use got or as @rob mentioned graphql-request which I’ve used in services
import { GraphQLClient } from 'graphql-request'
export const request = async (
query = {},
domain = process.env.HASURA_DOMAIN
) => {
const endpoint = `https://${domain}/v1/graphql`
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
'x-hasura-admin-secret': process.env.HASURA_KEY,
},
})
try {
return await graphQLClient.request(query)
} catch (error) {
console.log(error)
return error
}
}
to make some requests to a Hasura API I already had around.
So, could RW have pre-built some small clients to just set the endpoint and something to process the response? In the Hasura case, I just had to reference the key
import { requireAuth } from 'src/lib/auth.js'
import { request } from 'src/lib/hasuraClient'
export const stories = async () => {
requireAuth()
const query = `
{
stories {
author
categories
categoryGroup
channelEmojiIcon
channelId
channelName
companyNames
conceptEmojiIcons
.. lots more
url
}
}
`
const data = await request(query, process.env.HASURA_DOMAIN)
return data['stories']
}
and now can present the data in a cell just as normal.
But, back to functions.
- security
- http status codes
- parse/process request
- return result
I’d definitely like to see either via a generator or imports something to help with the above.
- some JWT and auth header helpers
- some basic small clients (http / gql) that can set the endpoint, auth, response handler
- handle response codes
- hook into processing result/response
the APIs that are available in the world are just so varied it’s hard to generalize them into a single pattern
Have to agree there.
I’ll end with one last idea that you brought up:
i could imagine a generator based on an OpenAPI spec (though that’s not universally adopted)? or perhaps just to reduce some of the boilerplate
I Googled/discovered this just now: Translate APIs described by OpenAPI Specifications (OAS) or Swagger into GraphQL.
Repo.
No idea how/if this works, but can see it accelerating the SDL generation and once you have a SDL can generate a service.
That would be interesting. I wonder if there is something similar for JSON Schema
Thanks for reading!