Javascript nodejs

Next JS and MongoDB CRUD Example

Next JS and MongoDB CRUD example by creating a simple To Do application.

The Next JS is a javascript framework that makes the d development of the React-based application easier. It also boosts the speed of static websites. In this article, we will learn how to create a simple Next JS and MongoDB CRUD application.

We will create a simple To-Do application using the Next JS and persist the To-Do items into the MongoDB NoSQL database.

We will also use Tailwind CSS for application styling.

Table of Contents

Creating the Next JS MongoDB CRUD app

We need to install Node JS in our system before proceeding with the creation of our Next JS app.

Create a new Next JS application with the tailwind CSS support. Run the below command in the command prompt/ terminal.

npx create-next-app -e with-tailwindcss to-do-app

The above command will create the basic folder structure of our application. It also installs the Tailwind CSS library into the application.

Also, install the MongoDB node package.

npm install mongodb

Below is the Next JS app folder structure.

next js folder structure

Few of the important files and folders here are:

  • public: This directory holds all static files of our application.
  • pages: Holds the route based files.
  • package.json: contains all the installed package details and scripts.
  • pages/index.tsx: This is the home page of our application.
  • pages/_app.tsx: This is the global app component that renders our application’s home page.

To run the application locally, use the below command.

npm run dev

Now the default server starts, and we can find this page below.

nextjs hello worls app

Update the content from the pages/index.tsx file as shown below.

import type { NextPage } from 'next'

const Home: NextPage = () => {
  return (
   <h2>Hello world!!</h2>
  )
}

export default Home

Now we get our classic Next JS “hello world” app.

next js hello world app

Creating the navigation bar for our app

Create a components folder, and inside that folder, create a navigation folder.

Create a navigation.tsx file as shown below.

import Link from "next/link"
import { Fragment } from "react"

const Navigation = () => {
    return (
        <Fragment>
            <ul className="flex bg-red-300">
                <li className="mr-6 px-4 my-5 font-bold hover:text-white">
                   <Link href="/">To Do APP</Link>
                </li>
                <li className="mr-6 px-4 my-5 font-bold hover:text-white">
                   <Link href="/add-todo">Add To Do</Link>
                </li>
            </ul>
        </Fragment>
    )
}

export default Navigation

We have Created a simple navigation bar with two links. The To-Do APP link is for the home page and the second link opens the Add To-Do page.

Update the pages/_app.tsx file as shown below.

We are adding the Navigation component before rendering any other component.

Also, To enable the tailwind CSS globally, we can import the tailwind CSS file inside the pages/_app.tsx file.

import '../styles/globals.css'
import 'tailwindcss/tailwind.css'
import type { AppProps } from 'next/app'
import { Fragment } from 'react'
import Navigation from '../components/navigation/navigation'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Fragment>
      <Navigation/>
      <Component {...pageProps} />
    </Fragment>
  )
}

export default MyApp

Create a add to do page by creating a add-todo folder under the pages folder and create a index.tsx file.

const AddTodo = () => {
    return (
        <h2>Add to do page!</h2>
    )
}

export default AddTodo

Run the application using the command “npm run dev

Now our navigation bar is ready with two items.

nextjs mongodb crud app
nextjs mongodb crud app

Adding the Create To Do functionality

Let us add the create To-Do functionality to our application. After the completion of this section, we will be able to create new To-Do Items using add to do page. The item gets saved to the MongoDB database.

Creating a To Do form

Inside the components folder, create a todoForm folder, and add a todoForm.tsx file inside it. We will use this component inside our add To Do page.

Add the below content.

import { useRef } from 'react'

function TodoForm(props: any) {

    const {addTodoHandler} = props

    const headingRef = useRef()
    const descriptionRef = useRef()

    const formSubmitHandler = (e: any) => {
        e.preventDefault()

        const formData = {
            heading : headingRef.current.value,
            description: descriptionRef.current.value
        }
        addTodoHandler(formData)
    }

    return (
        <form className="max-w-lg w-full mx-auto" onSubmit={formSubmitHandler}>
            <div className="flex flex-wrap mb-6 -mx-3">
                <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
                    To Do Item
                </label>
                <input className="appearance-none block w-full bg-gray-200 text-gray-700 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white"
                    type='text'
                    placeholder="heading"
                    ref={headingRef} />
            </div>
            <div className="flex flex-wrap mb-6 -mx-3">
                <label className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
                    Description
                </label>
                <input className="appearance-none block w-full bg-gray-200 text-gray-700 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white"
                    type='text'
                    placeholder="description"
                    ref={descriptionRef} />
            </div>
            <button
                className="px-4 py-2 my-1 font-semibold text-red-700 bg-transparent border border-red-500 rounded hover:bg-red-500 hover:text-white hover:border-transparent"
                type="submit">
                Add
            </button>
        </form>
    )
}

export default TodoForm

We are creating a form that expects two input data from the user. The title is the title of our To-Do item, and the description field is for a short description of the item.

We are using React’s userRef to extract the user input values. Once the user submits the form, the handler function is invoked, which we will create in the next step.

Our user form component is ready now. We can render this component inside out add To Do page.

Updating the add To Do page

Update the index.tsx file under the pages/add-todo with the below content.

import { useRouter } from "next/router"
import { Fragment } from "react"
import TodoForm from "../../components/todoForm/TodoForm"

const AddTodo = () => {
    const router = useRouter()
    const addTodoHandler = async (data: any) => {
        console.log("sending data::"+data)
        const response = await fetch("/api/new-todo", {
            method: "POST", 
            body: JSON.stringify(data),
            headers: {
                "content-Type" : "application/json"
            }
        }) 
        const res = await response.json()
        router.push("/")
    }

    return (
        <Fragment>
            <TodoForm addTodoHandler={addTodoHandler} />
        </Fragment>
    )
}

export default AddTodo

We render the ToDoForm component whenever the user visits the page /add-todo.

We have created a function with the name addTodoHandler. The function is invoked from the ToDoForm component that we created earlier.

Once the function is invoked, we use the Javascript fetch API to invoke the URI: /api/new-todo. Finally, we redirect the user to the home URL(/).

Creating the To Do creation API

Create a file called new-todo.tsx under the api folder and add the below content.

This file will contain the handler function that handles the To-Do Item create request.

Advertisements
import { MongoClient } from "mongodb"

async function handler(req: any, resp: any){

    if(req.method !== 'POST') return

    const {heading, description} = req.body
    const done = "false"

    if(!heading || !description) return
    
    const client = await MongoClient.connect("{mongo connection string}")
    const db = client.db()
    const collection = db.collection("todos")
    const result = await collection.insertOne({heading, description, done})
    client.close()

    resp.status(201).json({
        todo: result,
        message: "To do created"
    })
}

export default handler

We are validating the request and processing only the http POST request.

We are also validating the data before persisting it into MongoDB.

Also, we are using the MongoClient component from the node mongodb package to connect to the database and persist the data.

Testing the add To Do functionality

Now, if we run the application and navigate to the /add-to page, we get the below page.

nextjs mongodb crud app

Once we enter the To-Do item title and description and click on the Add button, data gets persisted to the MongoDB collection.

Displaying the To Dos from MongoDB

Now, let’s display the saved To-Do items on our application’s home page. Let’s create a new component that displays the To-Do items in a tabular format.

Creating the To Do Item component

Create a new folder under the component folder with the name todoItem and create a file todoItem.tsx

import { Fragment } from "react"

function TodoItem(props: any) {

    const { id, heading, description, done } = props
    return (
        <Fragment>
            <td className="py-5 font-bold text-blue-600">{heading}</td>
            <td className="py-5">{description}</td>
            <td className="py-5">
            {
            (done === "true") ? 
            (<img width="25" src="../../check.png" />) 
            :
            (<img width="25" src="../../uncheck.jpeg" />)}
            
            </td>
            <td className="px-4 py-2 my-1 font-semibold text-red-700 bg-transparent border border-red-500 rounded hover:bg-red-500 hover:text-white hover:border-transparent">
                <button className="id">Delete</button>
            </td>
        </Fragment>
    )
}

export default TodoItem

We have added some table data that displays the To-Do item details along with its status on the home page.

The component expects the id, heading, and description and done as component parameters.

We will implement the delete and update functionalities in the following section.

Using the component to display the To Do items on home page

Update the page/inex.html page with the below content.

We will use the TodoItem component to display the records fetched from the back-end MongoDB collection.

import { MongoClient } from 'mongodb'
import type { NextPage } from 'next'
import Head from 'next/head'
import { Fragment } from 'react'
import TodoItem from '../components/todoItem/todoItem'

const Home: NextPage = (props: any) => {  

  return (
    <Fragment>
      <Head>
        <link
          href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap"
          rel="stylesheet" />
      </Head>
      
      <div className="container mx-auto px-4 sm:px-8">
        <table className="min-w-full leading-normal">
          <thead>
            <tr>
              <th className="px-5 py-5 border-b-2 border-blue-200 bg-blue-100 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">To Do</th>
              <th className="px-5 py-5 border-b-2 border-blue-200 bg-blue-100 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Desc.</th>
              <th className="px-5 py-5 border-b-2 border-blue-200 bg-blue-100 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">Done</th>
              <th className="px-5 py-5 border-b-2 border-blue-200 bg-blue-100 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider"></th>
            </tr>
          </thead>
          <tbody>
            {
              props.todos.map((todo: any) => (
                <tr key={todo.id} className="px-5 py-5 border-b-2 border-black-200">
                <TodoItem
                  id = {todo.id}
                  heading={todo.heading}
                  description={todo.description}
                  done={todo.done} />
               </tr>
              )
              )
            }
          </tbody>
        </table>
      </div>
    </Fragment>
  )
}

export async function getStaticProps(context: any){

  const client = await MongoClient.connect("{mongo connection string}")
  const todoCollection = client.db().collection("todos")
  const todoArray = await todoCollection.find().toArray()
  client.close()

  return {
    props:{
      todos : todoArray.map(todo => ({
        heading: todo.heading,
        description: todo.description,
        done: todo.done,
        id: todo._id.toString()
      }))
    },
    revalidate: 60
  }
}

export default Home

We are using the getStaticProps function of the Next JS to pre-render the To-Do items from MongoDB during the build time.

Next JS uses the revalidate property to re-generate the page by rendering the data every 60 seconds.

Testing the retrieve to do functionality

Run the application, and now we can see the below page.

nextjs mongodb crud app

Implementing the delete functionality

Let’s add delete functionality to our To-Do application.

Creating the Delete API

Create a folder delete under api folder and create a file with the name [todoId].tsx

import { MongoClient, ObjectId } from "mongodb"

async function handler(req: any, resp: any){
    
    const {todoId} = req.query

    if(req.method !== 'DELETE') return
    
    const client = await MongoClient.connect("{mongo connection string}")
    const db = client.db()
    const collection = db.collection("todos")
    const result = await (await collection.deleteOne({_id: new ObjectId(todoId)})).deletedCount;
    client.close()

    console.log("deleted count::::"+result)

    return resp.json({
        todo: result,
        message: "To do deleted"
    })
}

export default handler

Here, the api/delete/{todoId} API is invoked by passing the To-Do item id. The API deletes the document by passing the given id.

Calling the delete function from To Do item component

We need to add the delete functionality by adding a new function to our TodoItem component. The function is invoked whenever the user clicks on the delete button of a To-Do item.

Modify the todoItem.tsx file with below content.

import { Fragment } from "react"
import { useRouter } from 'next/router'

function TodoItem(props: any) {

    const { id, heading, description, done } = props
    const router = useRouter()

    //delete todo:
    const deleteTodo = async (todoId: any) =>{
        
        const resp = await fetch(`/api/delete/${todoId}`, {
          method: 'DELETE'
        })
        .then(res => console.log("SUCCESS:: "+ res.json()))
        .catch(e => console.log("ERROR:" + e))
        
        router.push("/")
    }

    return (
        <Fragment>
            <td className="py-5 font-bold text-blue-600">{heading}</td>
            <td className="py-5">{description}</td>
            <td className="py-5">
            {
            (done === "true") ? 
            (<img width="25" src="../../check.png" />) 
            :
            (<img width="25" src="../../uncheck.jpeg" />)}
            
            </td>
            <td className="px-4 py-2 my-1 font-semibold text-red-700 bg-transparent border border-red-500 rounded hover:bg-red-500 hover:text-white hover:border-transparent">
                <button className="id" 
                onClick={() => deleteTodo(id)} >Delete</button>
            </td>
        </Fragment>
    )
}

export default TodoItem

Adding the functionality for To Do status update

We also have to provide the user an option to mark the To-Do item as done. Let’s implement this functionality now.

Creating the update to do API

Create a folder under api folder and a file […param].tsx

This API supports multiple HTTP path/request parameters.

import { MongoClient, ObjectId } from "mongodb"

async function handler(req: any, resp: any){

    if(req.method !== 'GET') return
    
    var query = { _id: new ObjectId(req.query.param[0].toString())};
    const options = {upsert: true }
    const updateTodo = {
         $set: { done: req.query.param[1] } 
      };

    const client = await MongoClient.connect("{mongo connection string}")
    const db = client.db()
    const collection = db.collection("todos")
    const result = await collection.updateOne(query, updateTodo, options);
    client.close()

    console.log("updated record::::"+result)

    return resp.json({
        todo: result,
        message: "To do updated!"
    })
}

export default handler

We are using the HTTP GET API to implement our update functionality, as we have to update only one field on our To-Do item.

We are using the upsert functionality to update the MongoDB record.

Calling the update to do API

To update the To-Do item status, we need to create a function to handle the user click events on the done table column.

Modify todoItem.tsx with below.

import { Fragment } from "react"
import { useRouter } from 'next/router'

function TodoItem(props: any) {

    const { id, heading, description, done } = props
    const router = useRouter()

    //delete todo:
    const deleteTodo = async (todoId: any) =>{
        
        const resp = await fetch(`/api/delete/${todoId}`, {
          method: 'DELETE'
        })
        .then(res => console.log("SUCCESS:: "+ res.json()))
        .catch(e => console.log("ERROR:" + e))
        
        router.push("/")
    }

    //togle todo:
    const togleDone = async (todoId: any, done: any) =>{
        
        const resp = await fetch(`/api/toggle/${todoId}/${done}`, {
          method: 'GET'
        })
        .then(res => console.log("SUCCESS:: "+ res.json()))
        .catch(e => console.log("ERROR:" + e))
        router.push("/")
    }
    
    return (
        <Fragment>
            <td className="py-5 font-bold text-blue-600">{heading}</td>
            <td className="py-5">{description}</td>
            <td className="py-5">
            {
            (done === "true") ? 
            (<img width="25" src="../../check.png" onClick={() => togleDone(id, "false")} />) 
            :
            (<img width="25" src="../../uncheck.jpeg" onClick={() => togleDone(id, "true")} />)}
            
            </td>
            <td className="px-4 py-2 my-1 font-semibold text-red-700 bg-transparent border border-red-500 rounded hover:bg-red-500 hover:text-white hover:border-transparent">
                <button className="id" 
                onClick={() => deleteTodo(id)} >Delete</button>
            </td>
        </Fragment>
    )
}

export default TodoItem

If the user clicks on the to-do item, the click event invokes the togleDone function. We are also passing the record’s id and the done status field so that we can update the To-Do item in MongoDB.

Testing the Next JS MongoDB CRUD application

Save the files and run the application(If it’s not running already).

We can now perform all the create/update/delete operations on our application, as shown below.

nextjs mongodb crud app

Conclusion

In this article, we learned how easy it is to create a Next JS and MongoDB CRUD application.

We also learned how Next JS makes it easier to create different application routes and supports static site generation to improve the application loading speed.

The code is available on Github.

Leave a Reply