- Last updated: October 2, 2024 (Strapi 5 era)
- 25 min read
How to Create a Book Rating App with Strapi Ratings Plugin and Cloudinary
Learn how to create a book rating application. This tutorial will introduce you to Cloudinary, Next.js, implementing a WYSIWYG editor, Strapi custom controllers, middleware, and policies.
Without a doubt, books are great. However, in some cases, we would like to give a review of books we have read. This is very important as we do not want to waste time on books that do not capture our interest.
In this tutorial, we will look at how to create a Book Rating application. It will introduce us to Cloudinary, Next.js, implementing a WYSIWYG editor, Strapi custom controllers, middleware, and policies.
Prerequisites
Before we start, we need to equip ourselves with the following:
- NodeJs installed on our local machine. See tutorial for installation guide.
- Basic understanding of Strapi - get started with this quick guide.
- Basic knowledge of Next.js
- Basic knowledge of Tailwind CSS
What is Next.js?
A fantastic React framework for creating extremely dynamic applications is Next.js. Pre-rendering, server-side rendering, automatic code splitting, and many more fantastic features are included right out of the box.
What is Tailwind CSS?
An efficient CSS framework for creating unique user interfaces is called Tailwind CSS. We directly write our CSS in our HTML classes while using Tailwind CSS. This is quite helpful as it eliminates the need to use a separate library or import an external stylesheet for UI designs.
What is Strapi?
Strapi is an open-source headless CMS based on Node.js that is used to develop and manage content using Restful APIs and GraphQL.
With Strapi, we can scaffold our API faster and consume the content via APIs using any HTTP client or GraphQL enabled frontend.
Scaffolding a Strapi Project
Scaffolding a new Strapi project is very simple and works precisely as installing a new frontend framework.
We will start by running the following commands and testing them out in our default browser.
1 npx create-strapi-app strapi-book-ratings --quickstart
2 # OR
3 yarn create strapi-app strapi-book-ratings --quick startThe command above will scaffold a new Strapi project in the directory you specified.
Next, run yarn build to build your app and yarn develop to run the new project if it doesn't start automatically.
The last command will open a new tab with a page to register your new admin of the system. Go ahead and fill out the form and click on the submit button to create a new Admin.
Installing Strapi Ratings Plugin
This plugin will allow us to add reviews to our application through some API routes. Run the command below to install:
1 npm install strapi-plugin-ratingsAfter installation, you should see a new tab for rating plugin.
Building the Book collection
Next, we will create a new Collection Type that will store the details of each book.
For this reason, we will create the fields:
title, info, creator, imageUrl and likes.
Click “continue”. This would open up a screen to select fields. For the title, info , creator and imageUrl we would choose the Text field. And for the likes field, we will select
Next, click on the “Advanced settings” tab to make sure that this field is “Required field”. This is so that the field will be required when creating a record.
When all is set and done we should have the following fields:
| Field Name | Field Type | Required | Unique |
|---|---|---|---|
| title | Short text | true | false |
| info | Short text | true | false |
| creator | Short text | true | false |
| imageUrl | Short text | true | false |
| likes | JSON | false | false |
Extending The User Collection Type
In the User collection type, add a Boolean field isAdmin. This will allow us to add a policy that will check if a user is an admin for any request to delete a book.
Ensure that this new field has a default value false.
Allowing Public Access
When we interact with an API, there are cases whereby it is restricted, accessible, or limited to some requests or route actions. For route actions or requests that should be public, go to Settings > Users & Permissions Plugin > Roles, then click on Public. We would allow access to find , and findOne for Book collection.
And allow count, find , getPageSize, getStats for Ratings Plugin. This is so that we can get the total number of ratings, find a ratings for a particular book, and get the rating statistics of a book without being logged in.
Next, we allow some route actions to authenticated users. We proceed by clicking the “Back” button, then the Authenticated Role. Now select the following for Book collection.
Next, we allow access to authenticated users to create a review, find a review, and get users’ reviews for a particular book.
Creating a Policy
Policies are executed before a middleware and a controller. Our Book Ratings application demands that only an admin can delete a book. Remember we added an additional boolean field isAdmin to the user collection. This would help us differentiate between a user and an admin.
Head over to the folder src/api/book and create a folder called policies. Inside it, create a file called is-admin.js. Next, add the following code to it:
1 // path: ./src/api/book/policies/is-admin.js
2
3 module.exports = async (policyContext, config, { strapi }) => {
4 // check if user is admin
5 if (policyContext.state.user.isAdmin) {
6 // Go to controller's action.
7 return true;
8 }
9 // if not admin block request
10 return false;
11 };- line 5: This checks if the user making the request is an admin. By default, the
isAdminisfalsefor every user. However, an admin will have atruevalue for theisAdminfield. - line 7: we Strapi to allow the request to head over to any controller that needs this policy.
- line 10: if the user is not an admin, we want to prevent or block the user from performing the request, in this case deleting a book.
Adding Policy to Delete Book Route
Next, we want to add this policy to the delete route of our application. We need to modify routes logic to achieve this. Head on to the file src/api/book/routes/book.js and add the following:
1 // path: ./src/api/book/routes/book.js
2
3 'use strict';
4 /**
5 * book router
6 */
7 const { createCoreRouter } = require('@strapi/strapi').factories;
8 module.exports = createCoreRouter('api::book.book', {
9 config: {
10 delete: {
11 // register policy to check if user is admin
12 policies: ["is-admin"]
13 },
14 }
15 });In the code above, we tell Strapi to configure the book router to register and enable the policy we created previously to work on the delete route of our book. Later, we will add some middlewares to the create and update middlewares of our application.
Adding Middlewares to Book Routes
At this point, we need to customize our book routes with some middlewares. Middlewares are functions performed before a request gets to a controller. In Strapi, 2 middleware exists. The one for the entire server application, and the other for routes. In this tutorial, we are using the latter. See middlewares for more on Strapi middlewares.
Prior to hitting the create , update and delete route actions or controllers, we want to be able to know and attach the username of an authenticated user to the request context. This is so that we can easily access it in the controllers. To do this, open the routes file at this location src/api/book/routes/book.js. Replace its content with the following code below:
1 // path: ./src/api/book/routes/book.js
2
3 'use strict';
4 /**
5 * book router
6 */
7 const { createCoreRouter } = require('@strapi/strapi').factories;
8 module.exports = createCoreRouter('api::book.book', {
9 config: {
10 create: {
11 middlewares: [
12 (ctx, next) => {
13 // check if user is authenticated and save username to context
14 let user = ctx.state.user;
15 if (user) ctx.username = ctx.state.user.username;
16 return next();
17 }
18 ]
19 },
20 update: {
21 middlewares: [
22 (ctx, next) => {
23 // check if user is authenticated and save username to context
24 let user = ctx.state.user;
25 if (user) ctx.username = ctx.state.user.username;
26 return next();
27 }
28 ]
29 },
30 delete: {
31 // register policy to check if user is admin
32 policies: ["is-admin"]
33 },
34 }
35 });In the code above, we configure our book router using the config options.
- line 10-19: we specify that authenticated users that want to create a book, we first get their details in line 14, then check if the user is actually authenticated in line 15. In the same line number, if user is authenticated, add a property to our request context called
username. This will help us when we customize our controller to be able to add value to thecreatorfield of a book. - line 20-19: same as line 10-19. This time we specify it for the
updatecontroller.
Creating a Custom like-book Route
Now, aside from adding a review and rating to our book. We want to allow users to like a book. Hence, the need for a custom route. This will be accompanied by a new custom controller. In this folder src/api/book/routes , create a file like-book.js. Add the following code:
1 // path: ./src/api/book/routes/like-book.js
2
3 module.exports = {
4 routes: [
5 {
6 method: "PUT",
7 path: "/books/:id/like",
8 handler: "book.likeBook",
9 config: {
10 middlewares: [
11 (ctx, next) => {
12 // check if user is authenticated and save username to context
13 let user = ctx.state.user;
14 if (user) ctx.username = ctx.state.user.username
15 return next();
16 }
17 ]
18 }
19 }
20 ]
21 }- line 6: we specify that this route accepts a
PUTrequest. - line 7: we indicate the path to reach this route. An example is http://127.0.0.1:1337/api/books/3/like. This will send a request to like a book with the id of 3.
- line 8: we specify the handler for this route. As we would see, this will be a custom handler in our book controller.
- line 9-18: we added a configuration with a middleware which checks if a user is authenticated and attaches their username to the request context, just as we did for the
updateandcreateof book routes.
Creating a Custom Controller
It is time to create handlers for our book controller. Open the file located in src/api/book/controllers/book.js and replace the code with the following:
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/43d5052455922f09ce21bd852d12db44
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/43d5052455922f09ce21bd852d12db44
- line 11 -51:
likeBookhandler was created for our like-book custom route. It first checks if the book exits. It then checks if a user has already liked a book through the JSON fieldlikeof our book content type by using thectx.usernamevalue which we passed as a middleware. If the user already liked the book, it removes the username of the user from the list. Otherwise, it adds it to the list. - line 53-62: we create the
createhandler for requests to create a book. It gets the details of the book we want to create. It also adds another detail to thecreatorfield of the book by getting the username of the user sending the request using thectx.usernamefrom the middleware we created earlier. - line 64-84: we create the
updatehandler for any request to update a book. It gets theidrequest parameter of the book and finds the book using thisid. It first checks if the book exists. It then checks if the user making the request is actually the creator of the request through thebook.creatorandctx.usernamevalues from the book found and the middleware. - line 86-101: the
deletehandler is created to delete a book. It also gets theidof the book from the request parameter and finds the book using thisid. Here we are not checking if the user is the creator of the book. This is because we already created a policy to allow only admin, who is a user with the fieldisAdminset totrue, to delete any book.
The full code to the backend server can be found on GitHub.
Scaffolding The Next.js App
Creating a NextJs Application
To create a NextJs application, we will cd into the folder of our choice through the terminal and run the command below:
1 npx create-next-app book-ratingsThe name of our application is book-ratings.
Now run the following command to cd into and run our application.
1 cd book-ratings
2
3 npm run devIf everything works out fine, this is what our browser opens up.
Now, open the folder in any code editor of your choice, and let's start coding the project together.
Installing Tailwind CSS
We will have to stop our application in order to install Tailwind CSS. We Press command c to stop it in our terminal. Next, we install Tailwind CSS. cd into the app folder and type the command below:
1 npm install -D tailwindcss postcss autoprefixer
2 npx tailwindcss init -pNext, we open our tailwind.config.js file which is in the root folder of our application, and replace the content with the following code.
1 // path: ./tailwind.config.js
2
3 /** @type {import('tailwindcss').Config} */
4 module.exports = {
5 content: [
6 "./pages/**/*.{js,ts,jsx,tsx}",
7 "./components/**/*.{js,ts,jsx,tsx}",
8 ],
9 theme: {
10 extend: {},
11 },
12 plugins: [],
13 }Finally, add this to the global CSS file at styles/global.css.
1 @tailwind base;
2 @tailwind components;
3 @tailwind utilities;Now restart our NextJs by running:
1 npm run devInstalling Other Dependencies
For our full-fledged application, we also require some npm packages. Run the command below to install them:
1 npm install axios cloudinary datauri js-cookie multer next-connect react-hot-toast react-icons react-quill- axios: this will help us interact with Strapi server API.
- cloudinary: this will allow us to upload book cover images to Cloudinary.
- multer: this is a middleware that will help us parse our image from the frontend of our application so that we can
- datauri: this will enable us to convert parsed images to a base 64 encodings. So that we can then upload the converted content to Cloudinary.
- js-cookie: this will help us create and retrieve cookies for authentication and authorization.
- next-connect: for handling file upload and creating a book record, we will use this in our NextJs handler. It will help us add the Multer middleware.
- react-hot-toast: this will be useful for toast notifications.
- react-icons: for icons in our application.
- react-quill: this will be used for our WYSIWYG editor and for previewing a raw HTML code.
Building Components in our NextJs App
NextJS is incredible with its component-based architecture, and we can develop our application by splitting the features into minor components. We open our code editor to proceed.
Layout Component
Firstly, create a new folder in the root directory called components. Inside it create a file called Layout.js. Add the following code. This will help us structure the layout of our application. We would need this late in _app.js of our file. Inside it, we import the Header component and Toaster from react-hot-toast. It is a high-order function that takes a children function and renders it.
1 import { Toaster } from 'react-hot-toast';
2 import Header from './Header';
3 export default function Layout({ children }) {
4 return (
5 <div>
6 <Header />
7 <Toaster />
8 <div className="content">
9 {children}
10 </div>
11 </div>
12 );
13 }
14 Header Component
Next, we create the Header component of our application. Inside the components folder create a file called Header.js and add the following code:
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/0ed5c73c116e8fc63d263d6e2a5091b5
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/0ed5c73c116e8fc63d263d6e2a5091b5
The code above displays the header of our application. In the code we imported the following:
1 import Cookies from 'js-cookie';
2 import AppContext from '../utils/AppContext';Cookies as can be seen in line 14 of the logout() function removes the cookie authToken. This is a token we set when a user logs in to our application. We also imported AppContext, a React context API, which includes authUser which contains the details of a logged in user. It contains setAuthUser which sets the authUser. isLoggedIn which is a Boolean value to indicate if a user is logged in. This context is created in the utils folder as would be seen soon.
We haven’t yet created the utils folder yet and will do so later in the tutorial.
Books Component
Also, inside the components folder create a file Books.js. This displays a list of the books users have created. It takes a prop called books which is the list of the books returned by the Strapi server end API. The prop is passed from the index.js page as would be seen later.
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/24a43943d26579c78ee1f972fe136d4a
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/24a43943d26579c78ee1f972fe136d4a
From the code above we imported the following:
1 import toast from 'react-hot-toast';
2 import axios from 'axios';
3 import Cookies from 'js-cookie';
4 import AppContext from '../utils/AppContext';
5 const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });toast will help us display toast notifications. axios will help us make a request to like a book.
We import Cookies in line 3 so that we get the authToken cookie.
- line 4: We also import the
AppContextso that we can get information about the logged-in user. - line 5: The
ReactQuillhere represents our WYSIWYG editor. Because thedocumentobject is needed byReactQuillduring page render, and is not available we used thedynamicprovided by NextJs to disable server-side rendering to fix this issue.
Also from the code above, we have a handleLikeBook() function which makes a request to like a book.
SingleBookAndReview Component
Lastly, inside the components folder, create a file SingleBookAndReview.js. This will display a single book when we request it. It will also display the reviews the average ratings of a book, comments, and ratings of each user that reviewed the book.
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/58b694bb0381888d47139c3431ac4182
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/58b694bb0381888d47139c3431ac4182
1 NEXT_PUBLIC_STRAPI_API=http://127.0.0.1:1337If you have not done so, go ahead and create .env file to store your variables.
Rebuilding the _app.js Page
This page initializes other pages in our application. We will need to update the content of the _app.js page. Replace the codes in it with the one below:
1 import '../styles/globals.css';
2 import { useState, useEffect } from 'react';
3 import axios from 'axios';
4 import Cookies from 'js-cookie';
5 import AppContext from '../utils/AppContext';
6 import Layout from '../components/Layout';
7 function MyApp({ Component, pageProps }) {
8 const [authUser, setAuthUser] = useState(null);
9 const [isLoggedIn, setIsLoggedIn] = useState(false);
10
11 const userAuthentication = async () => {
12 try {
13 const authToken = Cookies.get('authToken');
14 if (authToken) {
15 const { data } = await axios.get(`${process.env.NEXT_PUBLIC_STRAPI_API}/api/users/me`, {
16 headers: {
17 Authorization: `Bearer ${authToken}`,
18 },
19 });
20 setAuthUser(data);
21 setIsLoggedIn(true);
22 }
23 } catch (error) {
24 setAuthUser(null);
25 setIsLoggedIn(false);
26 }
27 };
28 useEffect(() => {
29 userAuthentication();
30 }, []);
31 return (
32 <AppContext.Provider value={{
33 authUser, isLoggedIn, setAuthUser, setIsLoggedIn,
34 }}
35 >
36 <Layout>
37 <Component {...pageProps} />
38 </Layout>
39 </AppContext.Provider>
40 );
41 }
42 export default MyApp;From the code above,
- line 6: we import the high-order
Layoutfunction component we created previously. - line 11-27: we create a function
userAuthentication()which checks if a user is authenticated by. If the user is authenticated, it sets the details of the user in the state variableauthUserand passes atrueBoolean value to theisLoggedInstate value. - line 28-30: we run this
userAuthentication()function any time a user accesses any page using the ReactuseEffecthook. - line 32-39: we wrap the whole codes inside of our React Context API. We then pass
authUser,isLoggedIn,setAuthUser,setIsLoggedIn, as global state variables using the Context API. Hence, we can access them anywhere in our applicatiion. - line 36-38: we wrap all pages of our application inside the
Layoutcomponent we created earlier on.
Building the Index.js Page
The index.js page will serve as our home page. Replace its content with the following code:
1 import Head from 'next/head';
2 import axios from 'axios';
3 import Books from '../components/Books';
4 export default function Home({ books, error }) {
5 if (error) return <div className="h-screen flex flex-col items-center justify-center text-red-500">Something went wrong!</div>;
6 return (
7 <div>
8 <Head>
9 <title>Book Ratings Application</title>
10 <meta name="description" content="Generated by create next app" />
11 <link rel="icon" href="/favicon.ico" />
12 </Head>
13 <div>
14 <div>
15 <Books books={books} />
16 </div>
17 </div>
18 </div>
19 );
20 }
21 export async function getServerSideProps() {
22 try {
23 const { data } = await axios(`${process.env.NEXT_PUBLIC_STRAPI_API}/api/books`);
24 return {
25 props: {
26 books: data.data,
27 error: null,
28 },
29 };
30 } catch (error) {
31 return {
32 props: {
33 books: null,
34 error: error.message,
35 },
36 };
37 }
38 }
39 - line 3: we import the
Bookscomponent. - line 21-38: we run a
getServerSideProps()function which will fetch all books we have created. And it will return this book as a prop to our home page. - line 15: we pass the props
booksreturned from thegetServerSideProps()to theBookcomponent. This is so that it will display all our books.
Here is what the home page looks like:
Building the Create Page
This is the page that will allow us to create a book for review.
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/93f991996acc00d3d44b31accd671020
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/93f991996acc00d3d44b31accd671020
- line 2: we import the CSS file for our Quill WYSIWYG editor.
- line 7: we import an icon from the
react-iconspackage. - line 9: we import
isImageandvalidateSizefunctions we created in thefileValidation.jsof ourutilsfolder. The former checks if a file is image. And the latter will check if a file is actually 5 megabytes in size. - line 12: we import the configurations for our WYSIWYG editor. These are found in the
utilsfolder of our app which we would create later. - line 27-63: we create the function
createBook()which sends the request to create a book. Note that it sends this to a NextJs route handler “/api/create-book”. We would create this route handler later in theAPIfolder of our application. This handler will handle image upload to cloudinary and creation of a book in NextJs server-side.
Here is what our create book page looks like:
Building the edit-book Page
This page allows us to edit a book. This would only allow the creator of the book to edit the book.
1 import { useState } from 'react';
2 import { useRouter } from 'next/router';
3 import axios from 'axios';
4 import 'react-quill/dist/quill.snow.css';
5 import Cookies from 'js-cookie';
6 import dynamic from 'next/dynamic';
7 import toast from 'react-hot-toast';
8 import { author_formats, author_modules } from '../utils/editor';
9 const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
10 export default function EditBook({ book, error }) {
11 const router = useRouter();
12 const successNotification = () => toast.success('Book updated successfully!');
13 const errorNotification = (error) => toast.error(error);
14 const [info, setInfo] = useState(book?.attributes?.info);
15 const [title, setTitle] = useState(book?.attributes?.title);
16 const updateBook = async () => {
17 const authToken = Cookies.get('authToken');
18 try {
19 const { data } = await axios.put(
20 `${process.env.NEXT_PUBLIC_STRAPI_API}/api/books/${book.id}`,
21 { data: { info, title } },
22 {
23 headers: {
24 Authorization: `Bearer ${authToken}`,
25 },
26 },
27 );
28 successNotification();
29 router.push(`/${book.id}`);
30 } catch (error) {
31 errorNotification(error.response.data.error.message);
32 }
33 };
34 return (
35 <div>
36 {error ? <div className="h-screen flex flex-col justify-center items-center text-red-500">{error}</div> : (
37 <div className="mx-5">
38 <h1 className="text-3xl font-bold">Edit this book</h1>
39 <div className="my-5 flex flex-col">
40 <label className="font-bold">Edit book title</label>
41 <input className="border my-3" value={title} onChange={(e) => { setTitle(e.target.value); }} />
42 </div>
43 <div className="my-5">
44 <label className="font-bold">Edit Book Info</label>
45 <ReactQuill className="w-full h-96 pb-10 my-3" onChange={setInfo} formats={author_formats} modules={author_modules} theme="snow" value={info} />
46 </div>
47 <button type="button" onClick={updateBook} className="shadow p-2 rounded bg-green-500 text-white font-bold">Update Book</button>
48 </div>
49 )}
50 </div>
51 );
52 }
53 export async function getServerSideProps({ query }) {
54 const bookId = query.book;
55 try {
56 const { data: book } = await axios(`${process.env.NEXT_PUBLIC_STRAPI_API}/api/books/${bookId}`);
57 return {
58 props: { book: book.data, error: null },
59 };
60 } catch (error) {
61 if (error.message === 'connect ECONNREFUSED 127.0.0.1:1337') {
62 return {
63 props: { book: null, error: 'Connection Error' },
64 };
65 }
66 return {
67 props: { book: null, error: error.response.data.error.message },
68 };
69 }
70 }
71 The code above basically updates a book.
Our edit book page looks like this:
Building the id.js Page
This page is responsible for viewing a particular book based on the id of the book. So when we visit want to see a book with the id of 2, we can just open the URL of our application and add the id. For example http://localhost:300/2.
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/f528d0c98be5bbba73a484f0fd013a02
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/f528d0c98be5bbba73a484f0fd013a02
For a book to be displayed, we need to fetch the book from our server together with its reviews. Hence in:
- line 92-115 we invoke the
getServerSideProps()function that will pass the book and its reviews and any error using theidwe passed to the URL. To get theid, we query the value from the request or URL usingcontext.queryvalue. - line 66: we pass this book and reviews we got to the
SingleBookAndReviewcomponent we created earlier on. - line 73: we allow users to add a review comment for this book using our
ReactQuillWYSIWYG editor. - line 76-83: also, we allow users to add a rating or score for the book.
- line 40-62: we create
addReview()function that will make a request to add a review to a book. It takes the comment from the user and the rating score. And sends the authorization token to the server. - line 85: we attach the
addReview()function to a button.
Here is what a single book and review page looks like:
Building our login and signup Pages
First, create the signup page by creating a file signup.js .
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/4b19b515c7b0d087ec607abc34d93659
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/4b19b515c7b0d087ec607abc34d93659
- line 55: we make a request for signup.
- line 61-63: if signup is successful, we create a cookie
authTokenwhich is thejwtreturned from the successful request. Also, we create another cookieuserthat will keep the user details. And we set these cookies to expire in 1 hour.
Our sign-up page looks like this:
Secondly, we create the login.js file.
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/353828b715a224a51be7ae389c73f0e2
https://gist.github.com/Theodore-Kelechukwu-Onyejiaku/353828b715a224a51be7ae389c73f0e2
This is basically the same as the signup.js page. The only difference is we are making a request to log in.
Here is what our login page looks like:
Creating create-book Route Handler
This is where we create a handler that will handle requests to create a book. Remember that in creating a book, we as well have to upload the book image to Cloudinary.
To proceed, we need a Cloudinary cloud name, API key, and API secret. Head over to the Cloudinary website to create an account and get these three.
Click on the dashboard tab. There, we will find our API key, API secret, and cloud name.
Click on settings, then click on upload. There, you can be able to add presets. Give the name and folder the value “flashcard” and click **save.**
Add Cloudinary details to .env.local file
Create a .env.local file and add the following.
1 // path: ./.env.local
2
3 NEXT_PUBLIC_STRAPI_API=http://127.0.0.1:1337
4 CLOUDINARY_API_KEY=<our_key>
5 CLOUDINARY_API_SECRET=<our_api_secret>
6 CLOUDINARY_NAME=<our_cloud_name>Now create the create-book.js file inside the api of the pages folder. Add the following code:
1 import nc from 'next-connect';
2 import multer from 'multer';
3 import DatauriParser from 'datauri/parser';
4 import axios from 'axios';
5 import path from 'path';
6 import cloudinary from '../../utils/cloudinary';
7 const handler = nc({
8 onError: (err, req, res, next) => {
9 res.status(500).end('Something broke!');
10 },
11 onNoMatch: (req, res) => {
12 res.status(404).end('Page is not found');
13 },
14 })
15 // uploading two files
16 .use(multer().single('image'))
17 .post(async (req, res) => {
18 const parser = new DatauriParser();
19 const { authToken } = req.cookies;
20 const image = req.file;
21 try {
22 const base64Image = await parser.format(path.extname(image.originalname).toString(), image.buffer);
23 const uploadedImgRes = await cloudinary.uploader.upload(base64Image.content, 'ratings', { resource_type: 'image' });
24 const imageUrl = uploadedImgRes.url;
25 const imageId = uploadedImgRes.public_id;
26 const { data } = await axios.post(
27 `${process.env.NEXT_PUBLIC_STRAPI_API}/api/books`,
28 {
29 data: {
30 info: req.body.info,
31 title: req.body.title,
32 imageUrl,
33 imageId,
34 },
35 },
36 {
37 headers: {
38 Authorization: `Bearer ${authToken}`,
39 },
40 },
41 );
42 res.json(data);
43 } catch (error) {
44 res.status(500).json({ error });
45 }
46 });
47 // disable body parser
48 export const config = {
49 api: {
50 bodyParser: false,
51 },
52 };
53 export default handler;From the code above:
- line 1-6: we import
next-connect, so that we can add Multer middleware to our handler.multerwill be used to parse our form data.datauri/parserwill be used to convert parsed image file to a base 64 encoding.pathfor getting the extension name of the parsed image file. And finally, our Cloudinary configuration file from theutilsfolder. - line 16: we passed the Multer middleware to parse any image with the form data name
imageusing thesingle()method. Note this was the name on thecreate.jspage. - line 23: we invoke Cloudinary to upload our image using the
uploader.upload()function. We passed it the image, and the upload preset “ratings” and specified that we want to upload an image using theresource_type: 'image'. - line 48-52: we tell NextJs to disable its body parser that we have a parser already which is Multer.
Creating Utilities for Our Application
So far we have imported some utilities in our components, pages, and API route handler. Let us create these utilities. First, we create folder utils at the root of our application
Context API Utility
This provides us with the ability to access states globally. Create a file AppContext.js and add the following code:
1 import { createContext } from 'react';
2 const AppContext = createContext({});
3 export default AppContext;Cloudinary Utility
Create a file cloudinary.js inside of the utils folder and add the following code:
1 import cloudinary from 'cloudinary';
2 cloudinary.v2.config({
3 cloud_name: process.env.CLOUDINARY_NAME,
4 api_key: process.env.CLOUDINARY_API_KEY,
5 api_secret: process.env.CLOUDINARY_API_SECRET,
6 });
7 export default cloudinary;
8 The code above configures our Cloudinary with our Cloudinary cloud name, API key, and API secret. Remember that this file was imported inside of the create-book request handler.
Editor Utility
This configures the WYSIWYG Quill editor in our pages and components. Create a file editor.js and add the following code:
1 export const author_modules = {
2 toolbar: [
3 [{ header: [1, 2, false] }],
4 ['bold', 'italic', 'underline', 'strike', 'blockquote'],
5 [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
6 ['link'],
7 ['clean'],
8 ],
9 };
10 export const author_formats = [
11 'header',
12 'bold', 'italic', 'underline', 'strike', 'blockquote',
13 'list', 'bullet', 'indent',
14 'link',
15 ];
16 export const reviewer_modules = {
17 toolbar: [
18 [{ header: [1, 2, false] }],
19 ['bold', 'italic', 'underline', 'strike', 'blockquote'],
20 [{ list: 'ordered' }, { list: 'bullet' }, { indent: '-1' }, { indent: '+1' }],
21 ['link', 'image'],
22 ['clean'],
23 ],
24 };
25 export const reviewer_formats = [
26 'header',
27 'bold', 'italic', 'underline', 'strike', 'blockquote',
28 'list', 'bullet', 'indent',
29 'link', 'image',
30 ];
31 - line 1 and 10: we define the editor for an author. This is the editor configuration for a user that wants to create a Book.
- line 16 and 25: we specify the editor for a reviewer. This is for a user to add a review for a book.
NOTE: the only difference is that author’s editor has
imagesupport removed. This is because we are using Cloudinary.
File Validation Utility
This file will be responsible for validating any image we want to upload. Create a file called fileValidation.js . Then add the following code:
1 export const validateSize = (file) => {
2 if (!file) return;
3 // if greater than 5MB
4 if (file.size > 5000000) {
5 return true;
6 }
7 return false;
8 };
9 const getExtension = (filename) => {
10 const parts = filename.split('.');
11 return parts[parts.length - 1];
12 };
13 export const isImage = (filename) => {
14 const ext = getExtension(filename);
15 switch (ext.toLowerCase()) {
16 case 'jpg':
17 case 'gif':
18 case 'bmp':
19 case 'png':
20 case 'jpeg':
21 return true;
22 default:
23 return false;
24 }
25 };
26 - line 1: the
validateSize()method will check if the image is less or equal to 5 megabytes and returns a corresponding Boolean resulttrueorfalse. - line 9: the
getExtensionhelps us get the extension of a file that we want to upload. It is called in theisImage()method in line 13. - line 13: we invoke the
isImage()function to check if the file we want to upload is an image.
NOTE: these methods are invoked in the pages of our application. A corresponding toast notification is displayed if the conditions are not met.
Testing Finished App
Our application looks like the one here:
Conclusion
In this tutorial, we have looked at how to create a Book Ratings application. We added policy, middleware, custom controllers, and routes to Strapi Server API. We were able to upload images to Cloudinary and utilize Strapi Ratings Plugin.
Strapi is great in so many ways. You only have to use it. Here is the full code to our application.
Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.