Tutorial updated by Fredrick Emmanuel and Paul Bratslavsky
What we are building:
This tutorial is part of the « Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe » tutorial series.
Table of contents
Note: The source code is available on GitHub here.
First of all, the list of restaurants needs to be displayed in our web app. Of course, this list is going to be managed through our API.
A Content-Type also called a model
, is a type of data. The Straps API includes by default, the user
Content Type. Right now a restaurant Content Type is needed, so the new Content Type is going to be, as you already guessed, restaurant
(Content-Types are always singular).
Here are the required steps:
+ Create new collection type
.restaurant
as the Display name.Continue
and create the followings fields:Text
called name
.Rich Text
and name it description
.Media
and name image
.At this point, your server should have automatically restarted and a new link Restaurants
appears in the left menu.
Well done! You created your first Content Type. The next step is to add some restaurants to your database. To do so, click on Content Manager then click on Restaurant in the left menu (http://localhost:1337/admin/content-manager/collectionType/api::restaurant.restaurant).
You are now in the Content Manager plugin: an auto-generated user interface that let you see and edit entries.
Create a restaurant:
Create New Entry
.Create as many restaurants as you would like to see in your apps.
Having the items in the database is great. Being able to request them from an API is even better. As you already know, Strapi's mission is to create API (I have got a super secret anecdote for you: its name is coming from Bootstrap your API 😮).
When you were creating your restaurant
Content-Type, Strapi created on the backend, a few sets of files in api/restaurant
. These files include the logic to expose a fully customizable CRUD API. The find
route is available at http://localhost:1337/api/restaurants. Visiting this URL will send a 403 forbidden error, as seen in the output below.
This is normal (Strapi APIs are secured by design). Don't worry, making this route accessible is super intuitive.
Public
role.find
and findone
checkboxes in the Restaurant
section.Authenticated
Now go back to http://localhost:1337/api/restaurants: at this point, you should be able to see your list of restaurants as shown in the output below.
By default, APIs generated with Strapi are REST endpoints. The endpoints can easily be integrated into GraphQL endpoints using the integrated GraphQL plugin.
npm install @strapi/plugin-graphql
Make sure to restart your strapi server if it does not auto restart. Restart your server, go to http://localhost:1337/graphql and try this query:
query Restaurant {
restaurants {
data {
id
attributes {
name
image {
data {
attributes {
url
}
}
}
}
}
}
}
You should get an output similar to the one below.
It looks like you are going in the right direction. What if these restaurants are displayed in our Next app?
Install Apollo in the frontend of our application, navigate to the /frontend
folder in a terminal window and type the following:
npm install @apollo/client graphql
We can implement our apollo client directly within the _app.js
file by replacing it with the following code.
1import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
2import "@/styles/globals.css";
3import Layout from "@/components/Layout";
4
5const API_URL = process.env.STRAPI_URL || "http://127.0.0.1:1337";
6
7export const client = new ApolloClient({
8 uri: `${API_URL}/graphql`,
9 cache: new InMemoryCache(),
10 defaultOptions: {
11 mutate: {
12 errorPolicy: "all",
13 },
14 query: {
15 errorPolicy: "all",
16 },
17 },
18});
19
20export default function App({ Component, pageProps }) {
21 return (
22 <ApolloProvider client={client}>
23 <Layout>
24 <Component {...pageProps} />
25 </Layout>
26 </ApolloProvider>
27 );
28}
Open the components folder and create a file named RestaurantList.jsx and add the following code.
Path: frontend/components/RestaurantList.jsx
1import { gql, useQuery } from "@apollo/client";
2import Link from "next/link";
3import Image from "next/image";
4import Loader from "./Loader";
5
6const QUERY = gql`
7 {
8 restaurants {
9 data {
10 id
11 attributes {
12 name
13 description
14 image {
15 data {
16 attributes {
17 url
18 }
19 }
20 }
21 }
22 }
23 }
24 }
25`;
26
27function RestaurantCard({ data }) {
28 return (
29 <div className="w-full md:w-1/2 lg:w-1/3 p-4">
30 <div className="h-full bg-gray-100 rounded-2xl">
31 <Image
32 className="w-full rounded-2xl"
33 height={300}
34 width={300}
35 src={`${process.env.STRAPI_URL || "http://localhost:1337"}${
36 data.attributes.image.data[0].attributes.url
37 }`}
38 alt=""
39 />
40 <div className="p-8">
41 <h3 className="mb-3 font-heading text-xl text-gray-900 hover:text-gray-700 group-hover:underline font-black">
42 {data.attributes.name}
43 </h3>
44 <p className="text-sm text-gray-500 font-bold">
45 {data.attributes.description}
46 </p>
47 <div className="flex flex-wrap md:justify-center -m-2">
48 <div className="w-full md:w-auto p-2 my-6">
49 <Link
50 className="block w-full px-12 py-3.5 text-lg text-center text-white font-bold bg-gray-900 hover:bg-gray-800 focus:ring-4 focus:ring-gray-600 rounded-full"
51 href={`/restaurant/${data.id}`}
52 >
53 View
54 </Link>
55 </div>
56 </div>
57 </div>
58 </div>
59 </div>
60 );
61}
62
63function RestaurantList(props) {
64 const { loading, error, data } = useQuery(QUERY);
65
66 if (error) return "Error loading restaurants";
67 if (loading) return <Loader />;
68
69 if (data.restaurants.data && data.restaurants.data.length) {
70 const searchQuery = data.restaurants.data.filter((query) =>
71 query.attributes.name.toLowerCase().includes(props.query.toLowerCase())
72 );
73
74 if (searchQuery.length != 0) {
75 return (
76 <div className="py-16 px-8 bg-white rounded-3xl">
77 <div className="max-w-7xl mx-auto">
78 <div className="flex flex-wrap -m-4 mb-6">
79 {searchQuery.map((res) => {
80 return <RestaurantCard key={res.id} data={res} />;
81 })}
82 </div>
83 </div>
84 </div>
85 );
86 } else {
87 return <h1>No Restaurants Found</h1>;
88 }
89 }
90 return <h5>Add Restaurants</h5>;
91}
92export default RestaurantList;
We have to make one more change in the next.config.js
file in order to make our images show up when using next js Image component.
Let's replace it with the following code.
Path: frontend/next.config.js
1/** @type {import('next').NextConfig} */
2const nextConfig = {
3 reactStrictMode: true,
4 images: {
5 remotePatterns: [
6 {
7 protocol: "http",
8 hostname: "localhost",
9 port: "1337",
10 pathname: "/uploads/**",
11 },
12 ],
13 },
14};
15
16module.exports = nextConfig;
You will have to stop and restart your frontend project before the changes take place.
Now let's replace /pages/index.js
file with the code below to display the Restaurant list and a search bar to filter the restaurants.
Path: frontend/pages/index.js
1import { useState } from "react";
2import RestaurantList from "@/components/RestaurantList";
3import Head from "next/head";
4
5export default function Home() {
6 const [query, setQuery] = useState("");
7 return (
8 <>
9 <Head>
10 <title>Create Next App</title>
11 <meta name="description" content="Generated by create next app" />
12 <meta name="viewport" content="width=device-width, initial-scale=1" />
13 <link rel="icon" href="/favicon.ico" />
14 </Head>
15 <main className="mx-auto container m-6">
16 <div className="mb-6">
17 <input
18 className="appearance-none block w-full p-3 leading-5 text-coolGray-900 border border-coolGray-200 rounded-lg shadow-md placeholder-coolGray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
19 type="text"
20 placeholder="Search restaurants"
21 onChange={(e) => setQuery(e.target.value)}
22 />
23 </div>
24 <RestaurantList query={query} />
25 </main>
26 </>
27 );
28}
Don't forget to create our Loader
component.
In components folder create a Loader.jsx file and add the following code:
1export default function Loader() {
2 return (
3 <div className="absolute inset-0 flex items-center justify-center z-50 bg-opacity-40 bg-green-500">
4 <div role="status">
5 <svg
6 aria-hidden="true"
7 className="inline w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-green-400"
8 viewBox="0 0 100 101"
9 fill="none"
10 xmlns="http://www.w3.org/2000/svg"
11 >
12 <path
13 d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
14 fill="currentColor"
15 />
16 <path
17 d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
18 fill="currentFill"
19 />
20 </svg>
21 <span className="sr-only">Loading...</span>
22 </div>
23 </div>
24 )
25}
Now you should see the list of restaurants on the page that are filterable!
Add more restaurants using Strapi Content Manager, the more the merrier.
Well done! 🍔 In the next section, you will learn how to display the list of dishes: https://strapi.io/blog/nextjs-react-hooks-strapi-dishes-3
Ryan is an active member of the Strapi community and he's been contributing at a very early stage by writing awesome tutorial series to help fellow Strapier grow and learn.