Calling Services from the web view, what am I doing wrong?

What I am trying to do is essentially fetch data from another endpoint, say a WordPress blog, and then loop through the results and create Articles/Posts in my RedwoodJS app. I don’t want to do this in the web client so I created a service to make the API call, then loop through my results and insert into my database.

// File: api/services/myservice.js

export const fetchArticles = async () => {
  // API call to fetch articles
  const articles = await apiFetchArticles()

  articles.forEach((article) => {
    // insert into database each article
    db.article.create({
      data: article,
    })
  })

  return articles
}

What I don’t understand is how can I call this from the frontend using Redwood? This is how I am used to doing things in the “react way”, where a button onClick handler executes.

// my component to initiate these service requests

export const Page = () => {
  const [articles, setArticles] = useState([])

  const generateArticles = async () => {
    // make API call to my service
    const { articles } = await fetchArticles()
    setArticles(articles)
  }
  return (
    <>
      <button onClick={generateArticles}>Generate Articles</button>
      {articles.map((item) => {
        return <li key={item.id}>{JSON.stringify(item)}</li>
      })}
    </>
  )
}

I think the “RedwoodJS way” is to use Cells and create a GraphQL SDL, but that seems like a lot of extra work just to make an API call? I already have an Article model defined with Prisma and I just want my api method to return a list of records it created in the database. I’m new to GraphQL so learning that and Redwood is a little cumbersome.

How can I easily call my service from the web? Do I have to use GraphQL or is there an easier option?

1 Like

Hello Spivey. I think you’re asking about how to call a third party API. If so, there is some information here that may be useful

Using a Third Party API | RedwoodJS Docs

That covers calling APIs from both the client side and the server side (and yes the server side is done through graphql as you mentioned)

I looked at that information and I’m doing the Server Side integration, but the part i’m failing to understand is how to execute that server side request from a frontend component?

Hi @Spivey and thanks for trying out RedwoodJS.

I plan to follow up tomorrow, but wanted you to know it hasn’t been missed.

Also, I thought I’d mention that I hold weekly open Office Hours with @KrisCoulson on Wednesdays at 12:30PM ET US time: Discord

Please feel free to stop by – sometimes asking in person can be more helpful, too.

I’ll just finish off with a few comments to think about and take the next step … what you are showing above says to me that you want to:

  1. There is a button on a page
  2. User clicks button
  3. Data fetched from an api
  4. Store the article content (I assume) in a data JSON column
  5. And then return the set of JSON data to present to the user

Few things:

  1. Why does the user need initiate that task with a button?
  2. Is a scheduled job a better approach?
  3. Could you do this using a database seed script instead? (Note: you can import that service in your seed script).
  4. The user will have to wait for fetching and persisting to finish.
  5. Do you want to upsert and not create? Will multiple button presses try to same the data. Since you don’t have a primary key, you may get lots of dupe article data stored.

What you might want to try quick is:

  1. Create a function “yarn rw g function fetchArticlesFromWordPress”
  2. In that function, import your service fetchArticles
  3. Then on your web side, make a request to fetchArticlesFromWordPress (similar to the weather example in the docs). Make sure you return the data. You just need to use what your apiUrl is configured to:
[web]
  apiUrl = "/.redwood/functions"

You should see a list of functions available and then `“/.redwood/functions/fetchArticlesFromWordPress”

The key is that a service isn’t exposed directly – it gets added to your schema (mapped to SDL if you have that) or you can use it in a function.

Just note that serverlesss functions are open api endpoints. You should consider securing them if needed: Serverless Functions (API Endpoints) | RedwoodJS Docs

But, I really suggest to give the GraphQL a try. It can seem strange at first and something new to learn, but once you get the hang of it – it’s really nice.

1 Like

Thanks @dthyresson you are correct in the flow in your first 1-5 bullets.

My example was for illustration purposes to demonstrate that I want to call a backend process from the frontend. I agree, something like importing WordPress posts into my RW app is better suited as a scheduled job, something I plan on implementing but this is all besides the point of trying to call my own server-side endpoint from the frontend.

Is the correct approach to use Cells to call GraphQL? I have no issues doing it the correct way besides me learning how to do it. I will have to check my code, but I think I had a Cell configured to use my custom GraphQL endpoint, but if memory serves me right the request was triggering when mounted and I don’t want to do that. I want to trigger on button click.

After thinking about this some more, I might shift gears and have RW provide the interface for updating my meta data, then have a scheduler run a script that uses my RW meta data to run the processing, instead of trying to call/execute any part of this from the UI.

1 Like

Sounds like a plan. Keep us posted!

I started a list of ways to kick off scheduled jobs that may help you decide how: How do I run background or cron jobs?

Just wanted to help give a more direct answer for future community members.

A cell is a great way to get data on load from the graphql server on initial render. If you have articles returned from your API you simply query it.

For example

// Article service

const articles = async () => {
  // Get articles from a database or 3rd party API
  const articles = await getArticles()
  
  return articles
}
// Article SDL
export const schema = gql`
  type Article {
    id: Int!
    title: String!
    body: String!
    createdAt: DateTime!
  }

  type Query {
    // Tell graphql you want a query with the same name as the export in your service
   // that returns a list of Articles which is defined above those are the available fields

    articles: [Article!]! @skipAuth 
  }

Okay now that the back end is done. We have an actual PUBLIC API that can be queried from any website, mobile app, or even your smart refrigerator.

Let’s look at hooking up the redwood js web side using a Cell

import Article from 'src/components/Article/Article'

export const QUERY = gql`
  query FindArticlesQuery {  // This can be any name but it used to cache the requests
    articles: { // Here we query the articles from our graphql server this new to match what we put in our sdl
      id
      title
      body
      createdAt
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div style={{ color: 'red' }}>Error: {error.message}</div>
)

export const Success = ({ articles }) => {
  return (
    <>
      {articles.map((article) => (
        <Article article={article} />
      ))}
    </>
  )
}

When you use a Cell you get to design all the lifecycle states of the request sent to the server without having to manually do it inside a single component full of ternary operators.

Okay, so that’s how you do it with a Cell but what if you actually really only want to get data when you click a button. Well we can still call our Graphql API but we need to useLazyQuery provided by apollo

import { useLazyQuery } from '@apollo/react-hooks'

export const QUERY = gql` // You still create a query
  query FindArticlesQuery {
    articles: {
      id
      title
      body
      createdAt
    }
  }
`

const Articles = () => {

  // useLazyQuery defers calling the query until you click the button 
  const [getArticles, { loading, error, data }] = useLazyQuery(QUERY) 

  if (loading) return <p>Loading ...</p>
  if (error) return `Error! ${error}`

  return (
    <>
      <button onClick={getArticles}>GET ARTICLES</button>
      {data?.articles?.map((article) => (
        <Article key={article.id} article={article} />
      ))}
    </>
  )
}

And that’s it you successfully talked to your API and guess what you don’t have to manage any of the data yourself or manage any react state for loading and storing the data and error messages.

Lastly, there are other things we can do like use a Cell to do the initial query. Then have a button on click that makes a mutation that goes to the database creates a bunch of new articles and then re-fetches the Cell data when the mutation is complete but ill save that for another time. I hope this helps and if anything is unclear. Let me know so I can clarify.

2 Likes

Hi @Spivey welcome to RedwoodJS

I happily use the RW cells to talk to all the backed things I want to do

Proxying anything with GraphQL is fairly easy, since you are the one sending the response & Redwood makes the call secure for you and handles everything about routing you to your own backend

Using the RW cells gave me the security over the request, and my AWS Lambda’s know it’s me asking

Al;

Cell:

export const QUERY = gql`
  query TheTemplatedSMSCell($phone: String!, $substitutions: String!, $templateName: String!) {
    templatedSMS(phone: $phone, templateName: $templateName, substitutions: $substitutions) {
      phone
      DeliveryStatus
      MessageId
      StatusCode
      StatusMessage
      TemplateName
      error
    }
  }
`
export const Empty = () => <span></span>
export const Loading = ({ variables }) => null // <span>Texting: {variables.phone}</span>
export const Failure = ({ error }) => <div style={{ color: 'red' }}>Error: {error.message}</div>
export const Success = ({ templatedSMS, variables }) => {
  variables.exfiltrate && variables.exfiltrate(templatedSMS)
  return variables.doneMsg ? <span>{variables.doneMsg}</span> : null
}
export const beforeQuery = (props: any) => {
  return { variables: props, fetchPolicy: 'cache-and-network', nextFetchPolicy: 'cache-first' }
}

SDL:

export const schema = gql`
  type TemplatedSMSResult {
    phone: String
    DeliveryStatus: String
    MessageId: String
    StatusCode: String
    StatusMessage: String
    TemplateName: String
    error: String
  }

  type Query {
    templatedSMS(
      phone: String!
      substitutions: String!
      templateName: String!
    ): TemplatedSMSResult @requireAuth
  }
`

Service:

const path = require('path')
const __file = path.basename(__filename)
import { logger } from 'src/lib/logger'
import { thenDebug, thenError } from 'src/lib/thenables'
import { smsViaTemplate } from 'src/lib/smsViaTemplate'
export const templatedSMS = async ({ phone, templateName, substitutions }) => {
  try {
    return await smsViaTemplate({ phone, templateName, substitutions }) 
      .then(thenError('templatedSMS'))
      .then(thenDebug(`templatedSMS`))
      .then((result) => ({
        ...result.smsResults,
        TemplateName: result.smsTemplateName,
      }))
      .then((result) => result[0])
      .then(thenDebug(`templatedSMS ~ smsViaTemplate`))
  } catch (err) {
    logger.error(`templatedSMS threw: ${JSON.stringify(err)} at: ${__file}`)
  }
}

Lib, smsViaTemplate

// https://www.npmjs.com/package/node-fetch
const fetch = require('node-fetch')
import { logger } from 'src/lib/logger'
import { getAwsUrl } from 'src/lib/utils'
// import { thenDebug } from 'src/lib/thenables'
export const smsViaTemplate = ({ phone, templateName, substitutions }) => {
    const awsUrl = getAwsUrl(process.env.arbitrarySMSPath)
    // logger.debug(`smsViaTemplate ~ awsUrl: ${awsUrl}`)
    return fetch(awsUrl, {
      method: 'post',
      headers: {
          Authorization: `Bearer ${process.env.bearerToken}`,
          'Content-Type': 'application/json',
      },
      body: JSON.stringify({ phone, smsTemplateName: templateName, smsTemplateSubstitutions: substitutions }),
    })
    .then((res) => res.json())
    // .then(thenDebug('json'))
}

Thanks everyone. @KrisCoulson solution is exactly what I was looking for, specifically useLazyQuery to trigger the query.

…and @ajoslin103 provided me with a better view of using GraphQL!

Just a follow up, I created a Task type to create Tasks from a web component which then calls a mutation on form submit. I created a “scheduleTask” mutation that calls a custom service that creates a new task, then executes a background task using Bull. I tried using Faktory but for some reason my background worker couldn’t connect to the Faktory instance.

I’m also sticking with GraphQL despite the “setup” taking some getting used to, I’m finding it very nice and convenient to pass around a single type into my various background processing without guessing what values to expect.

Sorry if my terminology is not accurate, hopefully my concept resonates despite my potentially wording confusion.

1 Like

This is so clear, it should really be somewhere in the official documentation. This answer could be a single page that so clearly and succinctly explains how to create and implement a simple service. Thank you for this!