How to Build a Full Stack Content Management System using Next.js, Xata, and Cloudinary
Tags: jamstack, javascript, serverless, webdev
Published: 2022-11-23 07:47:56

Managing a website's content is always challenging when developing a modern web application. You can manage the content via a database or a hard-coded JSON file. But, when creating large-scale projects, managing files and databases becomes complex. In these cases, using a [Content Management System](https://en.wikipedia.org/wiki/Content_management_system/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) (CMS) is one of the simple and efficient methods.

A CMS is software used to create and manage content with a team. There are many third-party CMS providers, such as Sanity and Strapi. However, someone once said, "If you want something done right, do it yourself."
In this article, I will teach you how to create your own CMS using the latest web development technologies. You'll learn to use a database and Cloudinary to manage your web app's content and digital assets.

![final demo of the application - ecommerce cms](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7q2d0h6liftbr4tn313b.png)

Here’s the [source code](https://github.com/giridhar7632/jamstackhack-22/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) and the [final demo](https://jamstackhack-22.netlify.app/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) if you want to dive directly into the code.

## Prerequisites

Before diving into this tutorial, you should:

- have a solid understanding of HTML and CSS
- know JavaScript's [ES6 syntax](https://www.w3schools.com/Js/js_es6.asp/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
- understand React and how it works

## What You Will Build

You will build a CMS for managing the content of an Ecommerce website. There are various [types of content management systems](https://www.oracle.com/content-management/what-is-cms/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog). In this tutorial, we'll create a headless CMS with a back-end system connecting to a database for managing content using a web interface. You will expose the content for the front end via API endpoints and use the data however you like.

## Technologies You Will Use

- [Next.js](https://nextjs.org/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a JavaScript framework for building FullStack Jamstack applications. It extends the capabilities of React for writing serverless code
- [Tailwind CSS](https://tailwindcss.com/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a utility-first CSS framework that uses classes to style the markup
- [Xata](https://xata.io/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a serverless database that lets you create Jamstack applications without worrying about deployment or scaling issues
- [Cloudinary](https://cloudinary.com/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) — a platform for managing media assets, such as images

You'll need [Cloudinary](https://cloudinary.com/users/register_free/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog), [Xata](https://app.xata.io/signin/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog), and [Netlify](https://app.netlify.com/signup/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) accounts to follow along. These services provide a generous free tier you can use for this project.

## Getting Started

To help you out, I created a [starter codesandbox](https://codesandbox.io/p/github/giridhar7632/ecommerce-cms-starter/main/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog); fork it and start coding.

Run the following command to start the local development environment.

```
yarn create next-app my-ecommerce-cms -e https://github.com/giridhar7632/ecommerce-cms-starter
# or
npx create-next-app my-ecommerce-cms -e https://github.com/giridhar7632/ecommerce-cms-starter
```

The starter code includes all essential dependencies, such as Tailwind CSS, Xata, and others. It also contains several ready-made frontend components styled using Tailwind CSS.
Navigate to the project directory and start your development server after installing the dependencies:

```
cd my-ecommerce-app

yarn dev
```

Now you will be able to see the app running at http://localhost:3000. You can also see the preview inside your codesandbox.

![preview of the starter project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8c49m6jijrbf3kslzfk3.png)

## Starter Code

I already created the UI of the application in the starter code to make the process simpler. The main things inside the project are:

1. **The** ` **/**``**pages** ` **directory:** Next.js allows [file system-based routing](https://nextjs.org/docs/routing/introduction/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog). It considers anything inside this directory as a route and the files inside `/pages/api` as API endpoints
2. **The** `**/components**` **directory:** If you are familiar with React, you may already know that everything is a reusable component. This directory contains components such as layouts, forms, and everything required. Take a look inside the folders to understand the structure
3. **The** `**/pages/products.js**` **file:** The route for displaying the products. You can find the components used here inside the `/components/Products` folder
4. **The** `**/pages/product**` **directory:** You can create dynamic routes for generating static pages using Next.js. You can define the route using the bracket syntax (`[slug]`) to make it dynamic. You can find the file `[id].js` inside this folder. So, you can get the `id` as a parameter inside that route

Let's configure the database and add API endpoints to make the UI interactive.

## Connecting to Database

[Xata](https://xata.io/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) is the serverless database that you will use to develop this CMS. Using Xata, you can store all the data inside a fully configured database without worrying about deployment and scaling.
Xata provides a CLI tool, `@xata.io/cli`, for creating and managing databases directly from the terminal.

Run the command below to install the Xata CLI globally and use it in your project.

```
yarn add -g @xata.io/cli
# or
npm i -g @xata.io/cli
```

After the installation, use the below command to generate an API key for Xata.

```
xata auth login
```

Xata will prompt you to select from the following options:

![Xata CLI asking to create API key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/va0m806wxan84vvpc19s.png)

For this project, select the **Create a new API key** **in the browser**; give the API key a name when the browser window pops up.

![Creating a new Xata API key inside browser](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v8bb4mqwt4xx3lloqcug.png)

Workspaces symbolize your team, and they are the starting point of Xata where databases are created. You can create one once you log in to your account. The data within the **database** is organized in the form of **tables**, which have **columns** that form a strict schema in the database.

![Preview of workspace and database in Xata](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pvzp1x3do3b4yhpuhzpv.png)

## Creating the Table

To add products to the database, begin by constructing a table and its schema for data structuring. The **products** table will have a few columns, such as:

- `id` — an automated column maintained by Xata
- `name` — a string containing the product's name
- `description` — a string with a description of the product
- `price` — a number containing the product value
- `stock` — the number of items remaining
- `thumbnail` — the image URL of the product
- `media` — multiple URLs to the media of the product

In Xata, you can construct a table using either the browser or your CLI. Now, let's build the table with the CLI. The database schema for this course is already inside the `schema.json` file of the starter code.

```json
/schema.json

{
  "formatVersion": "1",
  "tables": [
    {
      "name": "products",
      "columns": [
        {
          "name": "name",
          "type": "string"
        },
        {
          "name": "description",
          "type": "text"
        },
        {
          "name": "price",
          "type": "float"
        },
        {
          "name": "stock",
          "type": "int"
        },
        {
          "name": "thumbnail",
          "type": "string"
        },
        {
          "name": "media",
          "type": "multiple"
        }
      ]
    }
  ]
}

```

Now, execute the following command to populate the database with the given schema.

```bash
xata init --schema=schema.json --codegen=utils/xata.js
```

![Xata database creation process in CLI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3rfjrxrcjhnevvredr9.png)

To create a new database, you must first answer several questions in the terminal. Then the CLI reads the schema from the file and creates a database. It also generates a `/utils/xata.js` file for using the Xata client. You will use this client to communicate with Xata.

Use the `xata random-data` command to generate some random data inside your database. Open your [Xata workspace](https://app.xata.io/workspaces/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) inside the browser to see your database in action.

![Xata database with randomly generated data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/naku3vrl7zct3d09eeod.png)

## Creating Product Data

Now, let's develop API endpoints for interacting with product data. Inside the `/pages/api` directory, add a folder called `products` and file called, `createProduct.js`. You will use this file to handle the [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) request for creating a product within the database.

```js
/pages/api/products/createProduct.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // create method is used to create records in database
  try {
    await xata.db.products.create({ ...req.body });
    res.json({ message: "Success 😁" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};
export default handler;

```

The `Xata Client` provides methods for accessing the databases; you can use the names of the table to perform operations like `xata.db.<table-name>` for example. You can also create records inside a database using the `create()` method of the table.
You can see the form inside `/components/ProductForm` for adding the required product data. I used `react-hook-form` to handle the form data in multiple steps.

![Form for creating a new product inside the database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9pqji541ux82wzc6mp39.png)

## Uploading Images to Cloudinary

Before adding new product to database, you will first upload the image to Cloudinary and store the image link with product data. Once you have a Cloudinary account, follow the below steps to enable image upload.
There are two ways of uploading media to Cloudinary:

1. Signed presets
2. Unsigned presets.
   Create an unsigned preset to allow users to upload images to your cloud.
   Go to your Cloudinary Dashboard and navigate to `Settings > Upload > Add upload preset`.

![Adding a new upload preset inside Cloudinary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1taqsraq32i4e6xsw5zp.png)

Configure the **Name**, **Signing Mode**, and **Folder** in the upload preset. Adding a folder is optional, but I recommend you put all your uploaded images in one place. You can learn more about Cloudinary upload presets in [their documentation](https://cloudinary.com/documentation/upload_presets/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog).

![Configuring the new upload preset inside cloudinary](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o6kr2wug9octq3ad7ok1.png)

Once saved, go to the previous page, where you can find the new unsigned preset.

![The new preset added](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/knpn33oqzi1jsnrf4r4u.png)

With your new preset, you can now upload the images to Cloudinary from the frontend. The `ThumbnailUpload.js` file inside `/components/ProductForm` directory will handle the image upload and add the image URL to the product data. Add the following code inside the file.

```js
/components/ProductForm/ThumbnailUpload.js

import React, { useState } from "react";
import Button from "../common/Button";

const ThumbnailUpload = ({ defaultValue, setValue }) => {
  const [imageSrc, setImageSrc] = useState(defaultValue);
  const [loading, setLoading] = useState(false);
  const [uploadData, setUploadData] = useState();
  const handleOnChange = (changeEvent) => {
    // ...
  };

  const handleUpload = async (uploadEvent) => {
    uploadEvent.preventDefault();
    setLoading(true);
    const form = uploadEvent.currentTarget;
    const fileInput = Array.from(form.elements).find(
      ({ name }) => name === "file"
    );
    try {
      const formData = new FormData();
      // specifying cloudinary upload preset
      formData.append("upload_preset", "vnqoc9iz");
      for (const file of fileInput.files) {
        formData.append("file", file);
      }
      const res = await fetch(
        "https://api.cloudinary.com/v1_1/scrapbook/image/upload",
        {
          method: "POST",
          body: formData,
        }
      );
      const data = await res.json();
      setImageSrc(data.secure_url);
      // adding the thumbnail URL to te main form data
      setValue("thumbnail", data.secure_url);
      setUploadData(data);
    } catch (error) {
      console.log(error);
    }
    setLoading(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      // ...
    </form>
  );
};
export default ThumbnailUpload;

```

The code above does the following:

- extracts the file from the form input, sends a POST request to the Cloudinary API along with [Form data](https://developer.mozilla.org/en-US/docs/Web/API/FormData/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog), and returns the public URL of the file
- uses the `setValue` method of `react-hook-form` to add the image URL to the product form data
- And also updates the components' state to make UI interactive

Similarly, you can add multiple image uploads using `Promise.all` method to handle the product media.

```js
/components/ProductForm/MediaUpload.js

// ...
const MediaUpload = ({ defaultValues = [], setValue }) => {
  const [imageSrc, setImageSrc] = useState([...defaultValues]);
  const [loading, setLoading] = useState(false);
  const [uploadedData, setUploadedData] = useState(false);
  const handleOnChange = (changeEvent) => {
    // ...
  };

  const handleUpload = async (uploadEvent) => {
    uploadEvent.preventDefault();
    setLoading(true);
    const form = uploadEvent.currentTarget;
    const fileInput = Array.from(form.elements).find(
      ({ name }) => name === "file"
    );
    try {
      // adding upload preset
      const files = [];
      for (const file of fileInput.files) {
        files.push(file);
      }
      const urls = await Promise.all(
        files.map(async (file) => {
          const formData = new FormData();
          formData.append("file", file);
          formData.append("upload_preset", "vnqoc9iz");
          const res = await fetch(
            "https://api.cloudinary.com/v1_1/scrapbook/image/upload",
            {
              method: "POST",
              body: formData,
            }
          );
          const data = await res.json();
          return data.secure_url;
        })
      );
      setImageSrc(urls);
      setValue("media", urls);
      setUploadedData(true);
    } catch (error) {
      console.log(error);
    }
    setLoading(false);
  };
  return <form onSubmit={handleUpload}>...</form>;
};
export default MediaUpload;

```

Now, you should add the product to the database by sending a POST request to the endpoint you created.

```js
/components/Product/AddProduct.js

import { Close } from "../common/icons/Close";
import ProductForm from "../ProductForm";

const AddProduct = ({ props }) => {
  const [isOpen, setIsOpen] = useState(false);
  const handleClose = () => setIsOpen(false);
  const handleOpen = () => setIsOpen(true);
  const onFormSubmit = async (data) => {
    try {
      await fetch(`${baseUrl}/api/products/createProduct`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      }).then(() => {
        handleClose();
        window.location.reload();
      });
    } catch (error) {
      console.log(error);
    }
  };

  return (
    // ...
  );
};

```

Now, try to add new products using the form. You can see the files uploaded to your Cloudinary and records created in Xata.

![Xata database records created using the UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ams8cd7pskeln7tozcis.png)

## Querying the Product Data

For displaying all the products, you will use client-side rendering. To show the details of each product, let's use server-side rendering with dynamic routing. Create an API endpoint `getProducts.js` for getting all the product data.

> ⚠️ **Warning:** It is advised not to use Xata in client-side programming. It may reveal your API key while retrieving data from the browser. So, we'll get all the data from the Xata inside API routes and deliver it to the client.

```js
/pages/api/products/getProducts.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // getMany or getAll method can be used to create records in database
  try {
    const data = await xata.db.products.getAll();
    res.json({ message: "Success 😁", data });
  } catch (error) {
    res.status(500).json({ message: error.message, data: [] });
  }
};
export default handler;

```

Here, you used the `getAll` method of the table to fetch all the records at one time. Refer to the [Xata docs](https://xata.io/docs/quickstart/building-a-web-app-with-xata-and-nextjs#methods-to-query-the-database/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) for more ways of fetching data from a table.
Inside the `/pages/Products.js` file, add the following code for fetching the data from the API.

```js
 /pages/Products.js

import { useEffect, useState } from "react";
// ...

function Products() {
  const [loading, setLoading] = useState(false);
  const [products, setProducts] = useState([]);
  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const res = await fetch(`/api/products/getProducts`);
        const { data } = await res.json();
        setProducts(data);
      } catch (error) {
        console.log(error);
      }
      setLoading(false);
    };
    fetchData();
  }, []);
  return (
    // ...
  );
}

// ...
```

In the above code, you are fetching the data inside the `useEffect` hook from the API you created before. Save your code and head over to the browser. You can see the product data displayed in the form of a table.

![Displaying all the product data in the UI, fetching from database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ncvatptpjplssc5m98e.png)

You will be redirected to the `/product/<some-product-id>` when you click on the details. As of now, you'll see an empty page; let's move on to create the add the data.

## Generating Static Pages for Product Details

You can find the `/product` folder inside the `/pages` directory. As mentioned, I added the `[id].js` file to generate dynamic routes. You will access the product's id using the parameters of the route. Refer to Next.js [documentation](https://nextjs.org/docs/routing/dynamic-routes/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) for more information on dynamic.
In the following code, first, you are fetching all the items to add paths for `getStaticPaths`. Then, in `getStaticProps`, you get the `id` from the route parameters (`params`) and bring the single product data for passing the props to the page.

```js
/pages/product/[id].js

import React from "react";
import { getXataClient } from "../../utils/xata";
// ...

const xata = getXataClient();
// props passed in the server while generating the page
function Product({ product }) {
  return (
    // ...
  );
}
export default Product;

// fetching data from xata in server for generating static pages
export async function getStaticProps({ params }) {
  // getting filtered data from Xat
  const data = await xata.db.products
    .filter({
      id: params.id,
    })
    .getFirst();
  return {
    props: { product: data },
  };
}
// pre-rendering all the static paths
export async function getStaticPaths() {
  const products = await xata.db.products.getAll();
  return {
    paths: products.map((item) => ({
      params: { id: item.id },
    })),
    // whether to run fallback incase if user requested a page other than what is passed inside the paths
    fallback: true,
  };
}

```

Kill the terminal and again run the `yarn dev` command. Open your browser and try to see the details of any product. Now you can see the page ready with the data. Here's mine:

![Statically generated product details page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7y5zwg1cb2xxvc7qi99x.png)

## Updating the Product

Let's add the functionality of updating the product. Create an API route for updating data from serverless functions. Create a new file, `updateProduct.js`, for updating the data inside your products API directory. Just like creating, you can edit the records in Xata using the `update` method of the table. The `update` method will identify the record using it’s `id` and update the columns with the `data`.

```js
/pages/api/products/updateProduct.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // using update method to update records in database
  const { id, ...data } = req.body;
  try {
    await xata.db.products.update(id, { ...data });
    res.json({ message: "Success 😁" });
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: error.message });
  }
};
export default handler;

```

Navigate to `UpdateProduct.js` component inside `/components/Product` to add the function to update the product data. You will send a **PUT** request to the API endpoint along with the product id and updated data.

```js
/components/Product/UpdateProduct.js

import { baseUrl } from "../../utils/config";
// ...

const UpdateProduct = ({ product, ...props }) => {
  // ...
  const onFormSubmit = async (data) => {
    try {
      await fetch(`${baseUrl}api/products/updateProduct`, {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ id: product.id, ...data }),
      }).then(() => {
        handleClose();
        window.location.reload();
      });
    } catch (error) {
      console.log(error);
    }
  };

  return (
    // ...
  );
};
export default UpdateProduct;

```

Try updating the product data using the form. You will see the data updated on the product details page and Xata table.

## Deleting a Product

To delete a specific record from a database, you can use the `delete` method with the `id` of the product. Create a new file, `deleteProduct.js`, inside the `/page/api/products` folder.

```js
/pages/api/products/deleteProduct.js

import { getXataClient } from "../../../utils/xata";
const xata = getXataClient();
const handler = async (req, res) => {
  // use delete method for deleting the records in database
  const { id } = req.body;
  try {
    await xata.db.products.delete(id);
    res.json({ message: "Success 😁" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};
export default handler;

```

Now, add the `handleDelete` function inside the `DeleteProduct.js` component.

```js
/components/Product/DeleteProduct.js

import { baseUrl } from "../../utils/config";
// ...

const DeleteProduct = ({ productId }) => {
  // ...
  const handleDelete = async () => {
    try {
      await fetch(`${baseUrl}/api/products/deleteProduct`, {
        method: "DELETE",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ id: productId }),
      }).then(() => {
        handleClose();
        window.location.replace("/products");
      });
    } catch (error) {
      console.log(error);
    }
  };

  return (
    // ...
  );
};
export default DeleteProduct;

```

You can try deleting the products from the product details page. If it works, you are good to go.

## Deploying to Netlify

To make this app available throughout the internet, deploy it to a cloud service. Netlify is an ideal service provider for this project as it supports Xata and Next.js out of the box. Install the Netlify CLI globally using `npm` to deploy the app from your terminal. Log in to CLI using your Netlify account.

```bash
# installing the Netlify CLI globally
npm i -g netlify-cli

# logging into your Netlify account
ntl login
```

After successful login, try running `ntl init` inside the project folder to configure Netlify. Answer the prompted questions, and you will have the URL for your deployed site. Here’s [mine](https://jamstackhack-22.netlify.app/).

![Netlify configuration using netlify-cli](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9y1ei0u707bmxn42qi90.png)

If you face any issues, try to fix them following [this guide](https://www.netlify.com/blog/2020/11/30/how-to-deploy-next.js-sites-to-netlify/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog).

## Conclusion

In this tutorial, you have successfully created a CRUD app using Next.js, Xata, and Cloudinary. As Jamstack combines several technologies for markup and APIs, you can use other services besides what you used in this tutorial. You can take this tutorial further by creating the frontend of an e-commerce site using your preferred framework.

## Resources

- [Next.js documentation](https://nextjs.org/docs/getting-started/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
- [Xata documentation](https://xata.io/docs/overview/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
- [Cloudinary](https://cloudinary.com/documentation/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog) [documentation](https://cloudinary.com/documentation/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)
- [More about CMS](https://www.oracle.com/content-management/what-is-cms/?utm_source=hackmamba&utm_campaign=hackmamba-hackathon&utm_medium=hackmamba-blog)