NextJS and mongoDB application-v1

NextJS and mongoDB application-v1

How to build a full stack application

ยท

9 min read

The basic functioning of any software company out there is to perform CRUD operations. The more they work on providing simplistic user Experience, the complexity grew under the hood. But the takeoff foundation remains simple, even for developers.

Goal โ›ฐ๏ธ๐Ÿšต

In this blog, we will build a full stack application using nextJS and mongoDB. Here is the product of our effort https://goals-of-our-generation.vercel.app/. ๐Ÿ‘ˆ

This is the first blog of the series with minimal features. As the app evolves in further tutorials, there will be new functionalities and added complexity. We will explore many important functionalities of web ecosystem and there will a finished- working application with every blog.

Pre-requisites โœ‹

There are fair amount of pre-requisites to understand how this project is working, like:

Tech Stack ๐Ÿ”งโš™๏ธ

NextJS by Vercel has changed the way of developing web.

As we are going to use mongoDB, we can use template provided by Vercel as --with -mongodb or we can begin with npx create-next-app. Example templates provide little boost in development, plus they are up-to-date with the methods/code used in setting up the connection with xyz(mongodb, in our case)

Setting up MongoDB Atlas Cluster

To work with the template, we must have the uri string required to establish connection between our application and mongoDB atlas cluster. MongoDB Atlas is a cloud database service for MongoDB. This means that you get to store and access your data from a remote computer. This eliminates the process of setting up MongoDB locally on your computer.

To use MongoDB Atlas, you need to have an account. If you donโ€™t have one, create a free account. If already you have one, login from here. mongo createProject.png mongo clusterCreation.png

Get the uri string

Once you create an account, a project will be created and assigned a free MO Sandbox with a Shared cluster. Create user and password and add your device IP to access the cluster.

Connect to cluster.png Copy that connection String

Setting up project

To begin with, NextJS provide a command to build the scaffold of application and start development via starter templates and as we are using template for mongoDB integration. Create folder and change your directory to it and run this command

npx create-next-app --example with-mongodb nextjs-mongodb-v1

nextjs-mongodb-v1 is the < projectName >. This will create the basic template. Now go to the directory by cd nextjs-mongodb-v1.

Now add that connection string that you copied from mongo atlas dashboard in the env.local file by assigning it to variable MONGODB_URI and replace the with your password

and run npm run dev. This will start your development server in your local at port 3000 and if you can see this screen โฌ‡๏ธ, we are good to go.

nextJS+ MongoDB template.png

Styling via Tailwind and custom CSS๐Ÿ•ด๏ธ

We need to provide styling, of course, to the application and Tailwind CSS works just fine along with nextJS. We can install tailwind in many ways, but using tailwind css CDN link works just fine for now. Copy the CDN link and paste it inside of index.js.

Created a styles.css at the root folder to import it inside the _app.js file. This _app.js file is created to pass down basic styling to all the components. This is _app.js file.

import '../styles.css'

export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

๐Ÿ“Form Component

To perform CRUD operation, it is obvious that you need a form. Here is the code for the Form Component inside the directory /Components/Form/index.js

import { useState } from "react"
import { SuccessAlert, ErrorAlert} from "../Alert";

 export const GoalForm = () =>
{ const [error, setError] = useState('');
const [message, setMessage] = useState('');

const [name, setName] = useState('');
const [age, setAge] = useState('')
const [description, setDescription] = useState('')

const postGoal = async (e) =>
{
    e.preventDefault()

    setError('')
    setMessage('')

    if(!name || !age || !description) return setError('All Fields are necessary!')
    let goal = {
        name,
        age,
       description,
        date:new Date(),


}
    console.log(JSON.stringify(goal))
    console.log(goal)
    //console.log(JSON.parse(details))
    try {
        const response = await fetch('/api/goals', {
            method: 'POST',
            body: JSON.stringify(goal)
        })

            //get the data

            let data = await response.json()
    console.log( `this is from goal component ${data.message}`)
            if (data.success) {
                 // reset the fields
                 setName('');
                setAge('');
                setDescription('')
                 // set the message
                return setMessage(data.message)
            } else {
                return setError(data.message)
            }


    } catch (err) {
        console.error('error happened here', err)
    }

}
    return (
        <div className="m-60 p-10 mt-20 px-70 border-1  bg-blue-200  flex flex-col shadow-2xl rounded-lg w-11/12 inset-5">



                     {error ? <ErrorAlert error={error} /> : null}

                {message ? <SuccessAlert msg={message}/>: null}

        <strong className="underline ">Imagination is a powerful tool</strong>


        <form onSubmit={postGoal} className="form my-2 flex flex-col">

        <label className="p-2 decoration-solid    ">Your Life/Current Goal</label>
        <input className="form-input border border-gray-400 p-2 rounded-lg appearance-none focus:border-gray-500 focus:outline-none mb-2" name="description" value={description} onChange={(e)=>setDescription(e.target.value)} type="text" placeholder="Your Goal" />

                    <label className="p-2 decoration-solid    " >Name</label>
                    <input className="form-input border border-gray-400 p-2  rounded-lg appearance-none focus:border-gray-500 mb-2 focus:outline-none" name="name" value={name} onChange={(e)=>setName(e.target.value)}   type="text" placeholder="Your name" />         

                    <label className="p-2 decoration-solid">Your Age <i className="fa fa-twitter" aria-hidden="true"></i></label>
                    <input type="number" className="form-input border border-gray-400 p-2 mb-4 rounded-lg appearance-none mb-2 focus:border-gray-500 focus:outline-none" name="age" value={age} onChange={(e) =>setAge(e.target.value)}  placeholder="Your Age (years old)" />



            <button type="submit" className="rounded-full bg-blue-600  p-1 border border-gray-600">Submit</button>

    </form>

      </div>
)   
}

Alert Components

The components used to display the status of API call are stored in directory `/Components/Alert/index.js

import AlertStyle from './Alerts.module.css'
export const SuccessAlert = ({msg }) =>
{
    return (
        <div className={AlertStyle.successDiv}>
            โœ…  {msg}
        </div>
    )
}
export const ErrorAlert = ({error }) =>
{
    return (
        <div className={AlertStyle.errorDiv}>โ—โ—   {error}</div>
    )
}

styling for alertComponents is in Alerts.module.css

.successDiv{
background-color: rgb(21, 177, 21);
color:white;
font-size: 14px;
padding:10px;
margin-bottom: 10px;
border-radius: .25rem;
}
.errorDiv{
background-color: rgb(223, 65, 60);
color:white;
font-size: 14px;
padding:10px;
margin-bottom: 10px;
border-radius: .25rem;

}

Little cheating with mongoDB

To make this work, we are doing a brute force, hard document generation in the mongodb collection 'goals' using atlas cluster UI. explicit creation of goalsArray from Atlas cluster UI.png

๐ŸŒ /pages/api/goals

This is the api endpoint, where we will handle all the requests made to db via certain events. The GET method is used in the /goals age to render all the goals, for which, UI code is down below.


import clientPromise from '../../../lib/mongodb'


export default async function handler(req, res){

    switch(req.method){
        case 'POST':
            return addGoal(req, res)
        case 'GET' :
            return getGoals(req,res)
    }
}


async function getGoals(req, res){
    try {
        //connect to database
        console.log('hitting api')
 const client = await clientPromise;
 const db = client.db()
 const goal= await db.collection('goals').find()
 const response =await goal.next()
 const goalsArray = response.goalsArray
 console.log(`this is ${response}`)
console.log(response.goalsArray)
        return res.json({goalsArray})
    } catch (error) {
         // return an error
         return res.json({
            message: new Error(error).message,
            success: false,
        })
    }
}


async function addGoal (req, res){


    try {
        let goal = JSON.parse(req.body);
       // console.log(goal)

        let{ name, age, description} =goal

        if(!name || !age || !description){
            throw new Error("Invalid Request");
        }
        //connect to database
 const client = await clientPromise;
 const db = client.db()


//POST request
const response = await db.collection('goals').updateOne({}, { $push: { "goalsArray": goal } })
 console.log(response)

//return a message
return res.json({
    message: 'Details updated successfully',
    success: response
})
    } catch (error) {
        // return an error
        return res.json({
            message: new Error(error).message,
            success: false,
        })
    }
}

๐Ÿ“ˆ ๐Ÿ“ˆUI so far

UI screen 1.png UI screen 2.png

/pages/goals ๐Ÿ”†๐Ÿ”†

This page renders the list of goals. There are two components built to deliver the UI of this page.

import Head from 'next/head'
import GoalsList from '../Components/Goals/GoalsList'
import Link from 'next/link'
const Goals=()=>{
    return (
    <div className="container">
<Head>
        <title>All Goals</title>
        <link rel="icon" href="/favicon.ico" />
        <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />

   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossOrigin="anonymous"></link>

      </Head>

      <main className=' box-content  items-center px-10 py-4'>
        <h1 className="title box-content pt-4 ">Goals of our Generation ๐Ÿ‘€๐Ÿ‘€</h1>
        <strong >
            <img src="/pointer.png" alt="pointer icon" className="icon float-right" />
            <Link href='/goals' className="subtitle pt-2 float-right">
            You can add your goal to this list, here</Link>
            </strong>
        </main>
         <GoalsList/>
         <footer > 
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by Vercel 
        </a>
<strong> Made with โฃ๏ธ</strong>
        <a href="https://www.twitter.com/agrit_tiwari"> @agrit_tiwari </a>
      </footer>
    </div>)
}

export default Goals

GoalsList Component

TO render the goals in such a fashion that latest added goal is rendered at the top, we must blend custom css along with tailwind in example. Custom CSS also gives more freedom with developer tools as well. The components directory is like this:

Component Directory.png

/Components/Goals/GoalsList.jsx

import React, { useEffect,useState } from 'react'
import GoalCard from './GoalCard'
import styles from './Goals.module.css'

const GoalsList = () => {
    const [data, setData] = useState([])
    const [isLoading, setLoading] = useState(false)

useEffect(()=>{
    setLoading(true)
    fetch('api/goals')
      .then((res) => res.json())
      .then((data) => {
        setData(data.goalsArray)
        setLoading(false)
      })
},[])

if (isLoading) return <p>Loading...</p>
  if (!data) return <p>No profile data</p>
//console.log(data)
    return (
    <div className={` ${styles.list}`}>
{data.map(( goal,index)=>(<GoalCard key={index} goal={goal}/>))}    
    </div>
  )
}

export default GoalsList

/Components/Goals/GoalCard.jsx

import React from 'react'
import styles from './Goals.module.css'
const GoalCard = ({goal}) => {
  return (
    <div className={` ${styles.card}`}>
      <div className={`${styles.cardDiv1}`}>
        {console.log(goal)}
        <p>๐Ÿ‘‰ {goal.description}</p>
        <span>{goal.date.slice(4, 15)}</span>, <span>{goal.date.slice(16, 21)} IST</span>
      </div>
      <div className={`${styles.cardDiv2}`}>
        <strong>๐Ÿ—ฃ๏ธ{goal.name}</strong>
        <p>{goal.age} years old</p>
      </div>
    </div>
  )
}

export default GoalCard

/Components/Goals/Goals.module.css

.list{

    width:550px;
    list-style-type: none;
    display: flex; 
    flex-direction: column-reverse; 
    vertical-align: top;
    padding: 20px;
background-color: rgb(221, 148, 70);


}

.card{
background-color: rgb(172, 175, 177);
border-radius:20px;
padding:10px 4px 8px 4px;

margin: 4px;
}

.cardDiv1{
    margin:5px;;
    width: 98%;
    background-color: rgb(209, 206, 208);
    border-radius: 20px;
    padding: 6px;

}
.cardDiv1 > p{
    font-size: 20px;
   padding-bottom: 0%;
    border-radius: 10%;
    /* border: 1px solid rgb(46, 46, 46);  */
    text-decoration-color: rgba(0, 0, 0, 0.603);
    text-decoration: solid;
    text-shadow: darkblue;
    text-align: center;

}
.cardDiv1 > span{
    text-align: right;
}
.timezone{
    font-size: smaller;
}
.cardDiv2{
   margin: 7px;
   width: 98%;
    background-color: rgb(209, 206, 208);
    border-radius: 20px;
    padding: 2px;
    display: flex;
    padding: 6px;
}

.cardDiv2 > p{

    font-size: 14px;
    flex: 1;
    text-align: end;
    margin-right:2px ;
}

/pages/index.js

import Head from 'next/head'
import { GoalForm } from '../Components/Form'
import clientPromise from '../lib/mongodb'
import Link from 'next/link'


export default function Home({ isConnected }) {
  return (
    <div className="container ">
      <Head>
        <title>Write your Life Goal</title>
        <link rel="icon" href="/favicon.ico" />
        <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />

   <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossOrigin="anonymous"></link>

      </Head>

      <main className='box-content  items-center px-10'>
        <h1 className="title box-content  pt-4 ">
          Write about your life Goal ๐Ÿฅ‡๐Ÿ’ญ
        </h1>

        {isConnected ? (<div className="flow-root">
          <strong >
            <img src="/pointer.png" alt="pointer icon" className="icon float-right" />
            <Link href='/goals' className="subtitle pt-2 float-right">
            See the List of all wonderful Goals people are working on </Link>
            </strong>
        </div>

        ) : (
          <h2 className="subtitle box-content  ">
          There is no access to Dreamland
          </h2>
        )}
</main>
      <div>
        <GoalForm/>
      </div>
      <footer > 
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by Vercel 
        </a>
<strong> Made with โฃ๏ธ</strong>
        <a href="https://www.twitter.com/agrit_tiwari"> @agrit_tiwari </a>
      </footer>
    </div>
  )
}

export async function getServerSideProps(context) {
  try {
    await clientPromise
    // `await clientPromise` will use the default database passed in the MONGODB_URI
    // However you can use another database (e.g. myDatabase) by replacing the `await clientPromise` with the folloing code:
    //
    // `const client = await clientPromise`
    // `const db = client.db("myDatabase")`
    //
    // Then you can execute queries against your database like so:
    // db.find({}) or any of the MongoDB Node Driver commands

    return {
      props: { isConnected: true },
    }
  } catch (e) {
    console.error(e)
    return {
      props: { isConnected: false },
    }
  }
}

UI ๐ŸŽด๐ŸŽด/pages/goals.js

The UI of the following code is like this:

goals page.png

Deployed on Vercel๐ŸŒ

This is the link for version-1 towards building full fledged app on nextJS + mongodb.

nextjs-mongodb-v1.vercel.app or https://goals-of-our-generation.vercel.app/

๐ŸŒผ๐ŸŒผThanks for following along.

Thanks JC Smile for reviewing.

Did you find this article valuable?

Support Agrit Tiwari by becoming a sponsor. Any amount is appreciated!

ย