Building a Full-Stack Yelp Clone with React, GraphQL, and Hasura: Dune Edition

In this tutorial, we‘ll embark on an epic journey to build a Yelp-style app set in the universe of Frank Herbert‘s sci-fi masterpiece Dune. Our app will allow users to browse information about the planets of the Dune universe, read reviews from other users, and submit their own reviews. We‘ll build this using a cutting-edge stack of React, GraphQL, and Hasura.

As the Bene Gesserit saying goes:
"Survival is the ability to swim in strange water."

So let‘s dive into the strange waters of full-stack development and see what wonders await us!

The Spice Must Flow: Our Development Stack

To accomplish our mission, we‘ll be using the following technologies:

  • React: A JavaScript library for building user interfaces, which will form the foundation of our frontend.
  • GraphQL: A query language for our API, providing a more efficient and flexible alternative to REST.
  • Hasura: An open-source engine that provides instant GraphQL APIs over our Postgres database, handling the heavy lifting on the backend.
  • Apollo Client: A state management library that integrates seamlessly with React and GraphQL.
  • Postgres: A robust open-source database to store our planets and reviews.
  • Heroku: A cloud platform that will host our Hasura backend for us.

Together, these technologies form a powerful "unstoppable combination", much like the alliance between House Atreides and the Fremen. United in our stack, we‘ll be able to develop highly performant web apps faster than you can say "Muad‘Dib"!

Deploying Dreams: Setting Up Our Backend

The first step in our journey is to deploy our backend to Heroku. Using the intuitive interface on the Hasura website, we can spin up a new GraphQL backend in a matter of clicks, complete with a Postgres database and Heroku hosting. It‘s almost too easy – the Spacing Guild would surely approve of this instantaneous travel to the world of backend development!

Once our Hasura project is deployed, we can access the Hasura console to start working on our data model. We‘ll create two tables to start: a "planets" table to store key info about each planet (name, image, description, etc.) and a "reviews" table to hold user-submitted reviews for each planet.

The "planets" table might look something like this:

id (uuid) | name (text) | image_url (text) | description (text) 
----------|-------------|------------------|--------------------
a85ac88e  | Caladan     | caladan.jpg      | Home of House Atreides and known for its lush greenery and coastal beauty.
37f5e46d  | Arrakis     | arrakis.jpg      | A harsh desert planet and the only known source of the spice melange.
5e6c9b0f  | Giedi Prime | giedi-prime.jpg  | The industrial, toxic homeworld of the brutal House Harkonnen. 

And our "reviews" table will have a foreign key linking back to the associated planet:

id (uuid) | body (text)                      | planet_id (uuid) 
----------|----------------------------------|------------------
9bc201e1  | "A beautiful and calming place." | a85ac88e
08efe652  | "Dry and dangerous."             | 37f5e46d
51da3f4c  | "Only a Harkonnen could love it."| 5e6c9b0f 

Once our tables are set up, Hasura will auto-generate a GraphQL API for us, complete with queries, mutations and subscriptions! We can explore this API in the GraphiQL tab and start planning how we‘ll leverage this flexible data layer in our app.

The Sleeper Has Awakened: Initializing Our React App

With our backend deployed and our data model in place, we can begin building the frontend of our app in React. We‘ll use create-react-app to generate a skeleton React project:

npx create-react-app dune-yelp
cd dune-yelp

Then we‘ll install the dependencies we need, including Apollo Client for smooth GraphQL integration:

npm install @apollo/client graphql

We‘ll also set up some initial routes using React Router:

npm install react-router-dom

Now we‘re ready to start building out the pieces of our UI!

The Golden Path: Implementing Core Features

Let‘s review the core features our app needs:

  1. A homepage that displays a list of planets
  2. The ability to click on a planet to view more details and reviews
  3. A search bar to filter planets by name
  4. A form to submit new reviews for a planet
  5. Real-time updates when new reviews are added

We‘ll implement these features one-by-one, combining React components with GraphQL queries and mutations.

For the homepage, we‘ll make a query to fetch the list of planets:

const GET_PLANETS = gql`
  query GetPlanets {
    planets {
      id
      name
      image_url
    }
  }  
`;

Then we‘ll pass the result into a PlanetList component to render them:

function PlanetList() {
  const { loading, error, data } = useQuery(GET_PLANETS);

if (loading) return <p>Loading planets...</p>; if (error) return <p>Error fetching planets!</p>;

return ( <div className="planet-list"> {data.planets.map(planet => ( <Planet key={planet.id} name={planet.name} image={planet.image_url} />
))} </div> ); }

For the single planet page, we‘ll make another query to get the reviews associated with the planet:

const GET_PLANET_REVIEWS = gql`
  query GetPlanetReviews($id: uuid!) {
    planet: planets_by_pk(id: $id) {
      name
      image_url
      description
      reviews {
        id
        body  
      }
    }
  }
`;  

To add the search feature, we‘ll use Apollo‘s useLazyQuery hook to trigger the query when the user submits the search form:

const SEARCH_PLANETS = gql`
  query SearchPlanets($query: String) {
    planets(where: {name: {_ilike: $query}}) {
      id
      name
      image_url
    }
  }
`;

function PlanetSearch() { const [searchPlanets, { data }] = useLazyQuery(SEARCH_PLANETS);

return ( <div>
<form onSubmit={e => { e.preventDefault(); searchPlanets({ variables: { query: %${e.target.value}% } });
}} > <input type="text" placeholder="Search planets..." /> </form> <PlanetList planets={data && data.planets} /> </div> ); }

To let users add new reviews, we‘ll create a form that fires off a GraphQL mutation:

const ADD_REVIEW = gql`
  mutation AddReview($body: String!, $planet_id: uuid!) {
    insert_reviews_one(object: {body: $body, planet_id: $planet_id}) {
      id
      body
      planet_id
    }
  }
`;

function ReviewForm({ planetId }) { let input; const [addReview] = useMutation(ADD_REVIEW);

return ( <form onSubmit={e => { e.preventDefault(); addReview({ variables: { body: input.value, planet_id: planetId } }); input.value = ‘‘; }}
> <input ref={node => input = node} />
<button type="submit">Add Review</button> </form> ); }

Finally, to enable real-time updates, we‘ll convert our earlier reviews query into a subscription:

const REVIEWS_SUBSCRIPTION = gql`
  subscription ReviewsSubscription($id: uuid!) {
    planet: planets_by_pk(id: $id) {
      reviews {
        id
        body
      }
    }  
  }
`;

function PlanetReviews({ planetId }) { const { data, loading } = useSubscription( REVIEWS_SUBSCRIPTION, { variables: { id: planetId } }
);

return ( <div> {loading && <p>Loading reviews...</p>}
{data && ( <ul> {data.planet.reviews.map(review => ( <li key={review.id}>{review.body}</li>
))} </ul> )} </div>
); }

And with that, we have a fully functioning full-stack Dune-themed Yelp clone! The spice is flowing and the reviews must grow!

Not One Kanly Allowed: Custom Business Logic

But wait, there‘s more! House Corrino has decreed that no review shall contain the forbidden word "Kanly" (a Dune term for a feud or vendetta). To enforce this dictum, we‘ll create a Hasura Action that checks review text when submitted and rejects it if this word is found.

In the Hasura console, we define a new action that takes the review body as input:

type Mutation {
  AddReview (body: String!): AddReviewOutput
}

type AddReviewOutput { id: UUID!
}

Then we implement this in a serverless function:

const KANLY = /kanly/i;

function AddReview({ body }) { if (KANLY.test(body)) { return new Error(‘Kanly is a forbidden term!‘);
}

// Else add review to database // ... }

Now we can update our earlier mutation to invoke this action instead of writing directly to the database:

const ADD_REVIEW = gql`
  mutation AddReview($body: String!) {
    AddReview(body: $body) {
      id
    }
  }
`;  

With this custom logic in place, our app is now fit for the Emperor!

Long Live the Fighters: Next Steps

In this tutorial, we‘ve taken a whirlwind tour of how to build a feature-rich full-stack app with some of the most powerful and productive tools in web development today. Like Paul Atreides riding a sandworm for the first time, we‘ve harnessed these technologies to accomplish feats that once seemed impossible.

But the fight is far from over. Here are some ideas for how you might expand this app:

  • User accounts and authentication: Let users sign up, log in, and keep track of their reviews.
  • Upvotes: Allow users to upvote helpful or insightful reviews.
  • Review responses: Let planet "owners" respond to reviews of their planet.
  • Spice mining mini-game: Add a mini-game where users can send mining expeditions to Arrakis to harvest spice.
  • Interstellar mapping: Integrate a 3D star map that shows the locations of planets and the travel routes between them.
  • Sandworm-rider leaderboards: Create leaderboards for various Dune-themed games and quizzes.

The possibilities are as endless as the deserts of Arrakis! May your crysknives stay sharp and your maker hooks fly true!

"The mystery of life isn‘t a problem to solve, but a reality to experience."

  • Frank Herbert, Dune

Now stop worrying about the weirding way and start coding the Atreides way! The Kwisatz Haderach awaits!

Similar Posts