How To Build An Online Library In Next.js With Xata And Cloudinary

Ubaydah - Nov 23 '22 - - Dev Community

Jamstack, fully known as javascript, APIs, and markup, is one of the architectures revolutionizing the way we approach building faster, scalable, and secure websites. It provides many benefits, like better performance and excellent developer experience, making it easy to serve static content dynamically.
Managing media and databases in Jamstack architecture can become cumbersome, so there is a need for products like Xata, which provides a serverless database for Jamstack products, and Cloudinary, which makes it easy to manage media easily in the application.

In this tutorial, we’ll create an online library that allows us to upload, manage and get books from others using Next.js, Chakra UI, Xata, and Cloudinary.

What Is Xata?

Xata is our go-to serverless data platform that allows us to optimize our development workflow by managing, scaling, preventing downtime, caching, and maintaining our database. At the same time, we focus on developing the client side. It provides a powerful search engine, relational database, analytics engine, and more. It works with various javascript stacks like Nextjs, Svelte js, Node js, etc.

What Is Cloudinary?

Cloudinary is a media management platform that makes it easy to manage images, files, and videos for websites and mobile apps. It covers everything from uploading, storing, optimizing, delivery, and media transformation, and it provides SDK support in various languages.

Prerequisites

To follow this tutorial, we will need to have the following:

  • Understanding of JavaScript, Typsecript and Es6 features
  • Basic knowledge of React and Next.js
  • Basic knowledge of Chakra UI
  • Nodejs and npm installed on our PC
  • A Cloudinary account, signup is free
  • A Xata account signup is free

The project currently runs on code sandbox, and you can easily fork to get started.
It is deployed live on Netlify on this link.

This a picture of what we will be building

A web display of the bookly app

Now let’s dive into building our application.

Project Setup

We will run npm init to initialize a new project in any directory of our choice.

Let’s go to the project directory and run the following commands to install dependencies.

cd <project_name>

# Install dependencies
npm install react react-dom next

# Install devDependencies
npm install typescript @types/node @types/react @chakra-ui/react axios bcrypt nprogress yup -D
Enter fullscreen mode Exit fullscreen mode

We will create a new directory called pages and add an index.tsx file. In the index.tsx, let’s add a layout like this.


const Index = ({ }) => {
  return (
    <h1>Hello world</h1>
  )
};
Enter fullscreen mode Exit fullscreen mode

We then start the development server on http://localhost:3000/ by running the command below:

npx next dev
Enter fullscreen mode Exit fullscreen mode

Running this command will give us a blank page with “hello world” written.

Now, let’s set up the database of our application.

Setting up Xata and Database Schema

To get started with Xata, we will create a new database on the Xata workspace by giving it a name and selecting the region we want our database to be.

A picture of Xata workspace

A picture of selecting database region in Xata

Next, let’s start building our database schema by adding new tables.

Xata Schema workspace

Our Schema Diagram looks like this:

Schema diagram drawn on drawsql app

We then create two tables called User and Book on Xata workspace and add the following fields in our schema above.
Xata lets us relate two tables with the type Link, so we use that when adding the field added_by and link it to the User table.

Now, let’s initialize our database in our project.

To start working with Xata on our Command line interface (CLI), we will install it globally by running the command.

# Install the Xata CLI globally
npm i -g @xata.io/cli
Enter fullscreen mode Exit fullscreen mode

Then let’s run the following command to initialize the created database.

xata init --db https://Ubaydah-s-workspace-qb9vvt.us-east-1.xata.sh/db/bookly
Enter fullscreen mode Exit fullscreen mode

Note: We can access our DB base URL from our workspace's configuration section. It follows the format https://Ubaydah-s-workspace-qb9vvt.{region}.xata.sh/db/{database}.

Xata prompts us and generates a new file for our Xata configuration.

Xata codegen on CLI

During the process, it prompts our browser to open so we can create an API key for our project, which will be added to our .env file and needed too when deploying.

Xata prompting browser to generate API key

Codes are generated in our xata.ts file, which contains the Schemas in codes and instances of XataClient, which we will use to interact with the Xata database.

We are all set. Let’s dive into building our APIs.

Building The APIs

In our pages directory, let’s create a new folder named api and two folders in the api folder named book and user.

Login And Register Functionality

Let’s create two new files in our user folder, register.ts, and login.ts.

In the register.ts file, let’s add the following code.


import { NextApiHandler } from "next";
import { getXataClient } from "../../../utils/xata";
import bcrypt from "bcrypt";
import { promisify } from "util";

const hash = promisify(bcrypt.hash);
const handler: NextApiHandler = async (req, res) => {
  const { email, username, password } = req.body;
  const xata = getXataClient();
  //validate
  const emailExists = await xata.db.User.filter({ email }).getFirst();
  const usernameExists = await xata.db.User.filter({ username }).getFirst();
  if (usernameExists) {
    return res.status(400).json({ success: false, message: "Username already exists! " });
  } else if (emailExists) {
    return res.status(400).json({ success: false, message: "User with the email already exists!" });
  }
  // user doesn't exist
  await xata.db.User.create({
    username: username,
    email: email,
    password: await hash(password, 10),
  });
  return res.status(201).json({
    message: "user created",
  });
};
export default handler;
Enter fullscreen mode Exit fullscreen mode

Explanation: In the code above, we created a NextApiHandler that handles creating new users in our database. We first checked if the email or username exists in the database and throw an error based on that. Then, if the user doesn't exist in the database, we hashed their passwords using the bcrypt package and created a new instance of them in the database.

Let’s add our Login API.

In the login.ts file, let’s add the following code.


import { NextApiHandler } from "next";
import { getXataClient } from "../../../utils/xata";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import { promisify } from "util";
import cookie from "cookie";
const { JWT_SECRET_KEY } = process.env;
const compare = promisify(bcrypt.compare);
const handler: NextApiHandler = async (req, res) => {
  const { email, password } = req.body;
  // instantiate a xata client
  const xata = getXataClient();
  const user = await xata.db.User.filter({ email }).getFirst();
  // validate
  if (!(user && (await compare(password, user.password)))) {
    return res.status(400).json({ success: false, message: "Username or password is incorrect!" });
  }
  // create a jwt token that is valid for 7 days
  const token = jwt.sign({ sub: user.id }, JWT_SECRET_KEY, { expiresIn: "7d" });
  user.update({ token: token });
  // set the jwt token as a cookie
  res.setHeader(
    "Set-Cookie",
    cookie.serialize("token", token, {
      maxAge: 24 * 60 * 60, // 1 day
      sameSite: "strict",
      path: "/",
    })
  );
  return res.status(200).json({
    id: user.id,
    username: user.username,
    email: user.email,
    token: token,
  });
};
export default handler;

Enter fullscreen mode Exit fullscreen mode

Explanation: In the code above, we filtered out the user from the database, checked if the user exists and if the password entered correlated with the hashed password. If the user exists, we created a new Jwt token for the user, valid for seven days, and updated their details with the token. We then set the Jwt token as a cookie in the header, where it will expire in a day on the browser.

Adding The Database CRUD Functionality

Now, let’s write the API to create, delete, update and get books from our database.

In the api/book directory, let’s create four new files named create.ts, update.ts, get.ts, and delete.ts.

In the create.ts file, let’s add the following code:


import { NextApiHandler } from "next";
import { getXataClient } from "../../../utils/xata";
const handler: NextApiHandler = async (req, res) => {
  // instantiate a xata client
  const xata = getXataClient();
  const book = await xata.db.Book.create(req.body);
  res.status(201).json({
    data: book,
  });
};
export default handler;

Enter fullscreen mode Exit fullscreen mode

In the code above, we created a new entry of our book gotten from the request body in our database.

In the update.ts file, let’s add the following code:


import { NextApiHandler } from "next";
import { getXataClient } from "../../../utils/xata";
const handler: NextApiHandler = async (req, res) => {
  const xata = getXataClient();
  const book = await xata.db.Book.update(req.body);
  res.status(201).json({
    data: book,
  });
};
export default handler;

Enter fullscreen mode Exit fullscreen mode

In the code above, we updated an entry of our book with the request body in our database.

In the delete.ts file, let’s add the following code:


import { NextApiHandler } from "next";
import { getXataClient } from "../../../utils/xata";
const handler: NextApiHandler = async (req, res) => {
  const { id } = req.body;
  const xata = getXataClient();
  await xata.db.Book.delete(id);
  res.end();
};
export default handler;

Enter fullscreen mode Exit fullscreen mode

Here, we deleted an entry in the database by passing the book id gotten from the request body.

In the get.ts file, let’s add the following code:


import { NextApiHandler } from "next";
import { getXataClient } from "../../../utils/xata";
const handler: NextApiHandler = async (req, res) => {
  const xata = getXataClient();
  const books = await xata.db.Book.getMany();
  res.status(200).json({
    data: books,
  });
};
export default handler;

Enter fullscreen mode Exit fullscreen mode

Here, we queried the xata database to get all entries from the database.

We can learn more about different ways of querying data in the Xata docs.

Search Functionality

In the api/book directory, let’s create a new file named search.ts and the following code:


import { NextApiHandler } from "next";
import { getXataClient } from "../../../utils/xata";
const handler: NextApiHandler = async (req, res) => {
  const xata = getXataClient();
  const books = await xata.db.Book.search(req.body.value, {
    fuzziness: 1,
    prefix: "phrase",
  });
  res.status(200).json({
    data: books,
  });
};
export default handler;

Enter fullscreen mode Exit fullscreen mode

In this code, we performed a full-text search on the Book table to get entries that matched the value passed. Xata tolerates typos when searching, and this behavior is enabled with the fuzziness parameter added in the code above. The set number 1 indicates one typo tolerance, and fuzziness can be disabled by setting it to 0.

Let’s learn more in xata search docs.

After building the essential APIs, we set up the Cloudinary account to store our documents.

Setting up a Cloudinary account

We will create an account on Cloudinary and be redirected to a dashboard. Let’s copy our cloud name and store it in our .env file.

cloudinary dashboard

Let’s hover over the settings section and go to the upload panel to create an upload preset using the unsigned key for uploading our documents.

Image description

Image description

After, let’s go over the security section to enable PDF delivery with the account.

Image description

We are now set to use the account to upload PDFs.

Building Frontend and Consuming APIs

In the root directory, let’s create a new folder named src, we will be creating our react components and hooks here. Create new folders named components, config, and hooks. The config folder will have a config.ts file in which our Cloudinary cloud name will be stored.


const config = {
  cloudinary_name: process.env.NEXT_PUBLIC_CLOUD_NAME,
};
export default config;
Enter fullscreen mode Exit fullscreen mode

In the hooks folder, let’s create a new file called useUploadImage.tsx and add the following codes:


import { useState } from "react";
const useUploadImage = () => {
  const [preview, setPreview] = useState<string>("");
  const [file, setFile] = useState(null);
  const onChange = (e: any, multiple?: any) => {
    //check if files are multiple and collect them in an array
    if (multiple) {
      const selectedFIles: any = [];
      const targetFiles = e.target.files;
      const targetFilesObject = [...targetFiles];
      targetFilesObject.map((file) => {
        return selectedFIles.push({ file: file, url: URL.createObjectURL(file) });
      });
      setPreview(selectedFIles);
    } else {
      const file = e.target.files[0];
      const reader: any = new FileReader();
      setFile(e.target.files[0]);
      reader.onloadend = () => setPreview(reader.result);
      reader.readAsDataURL(file);
    }
  };
  const image = {
    preview,
    file,
  };
  return { onChange, image };
};
export default useUploadImage;
Enter fullscreen mode Exit fullscreen mode

The code above handles collecting all the files sent on the client side so we can easily send them to Cloudinary.

In the components folder, we will create a new component named BookForm.tsx , where we will handle creating and updating books and sending them to the Next.js server.

In the code above, we created some form validation using yup. We then created some initial values for our book. The useEffect gets the current user logged in and stores it on our local storage. The function that handles sending the book to the server has two parameters: the data sent out and isEdit. The isEdit helps us differentiate whether we are adding a new book or updating it. A new FormData is then created where the file and upload_preset in sent to the Cloudinary API using Axios. Our secure URL is gotten from Cloudinary response, which is sent along with other data to the API to create a new book.

View the full codes on the GitHub repository to check other implementations like the login and register form.

The code also runs on code sandbox, and it’s deployed on Netlify.

Conclusion

In this tutorial, we learned about serverless databases and how to create one using Xata. We also learned about Cloudinary and how to upload files in Cloudinary using upload presets.

Next Steps

Check out the full GitHub repository to see other implementations and components built.
We can also add extra features such as displaying the user profile, bookmarking a book to a list, and other ideas to improve the app.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .