Table of contents
- Goal โฐ๏ธ๐ต
- Pre-requisites โ
- Tech Stack ๐งโ๏ธ
- Setting up MongoDB Atlas Cluster
- Setting up project
- Styling via Tailwind and custom CSS๐ด๏ธ
- ๐Form Component
- Alert Components
- ๐ /pages/api/goals
- ๐ ๐UI so far
- /pages/goals ๐๐
- GoalsList Component
- /pages/index.js
- UI ๐ด๐ด/pages/goals.js
- Deployed on Vercel๐
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:
- Understanding of Javascript
- Basics of React and state management
- Basic knowledge of mongoDB, the understanding of mongoDB will improve as project evolves
- Node.js installed. Node.js will help us run NPM commands to install any necessary dependencies that will help us build our Next.js application.
- Since we are using NextJS, prior experience/exposure to this framework will be helpful
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.
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.
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.
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.
๐ /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
/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:
/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:
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.