18
loading...
This website collects cookies to deliver better user experience
NextJS: A Full Stack framework built around React offering Client Side, Server Side and Static Rendering.
Typescript: A Javascript superset made by microsoft to write scalable code
Mongo: A Document Database
generate a new typescript next project with the following command: npx create-next-app --ts
cd into the new project folder and run dev server npm run dev
checkout that site is visible on localhost:3000
.env.local
(Next already knows to look for this file for environmental variables). Add the following with your desired database URI:DATABASE_URL=mongodb://localhost:27017/next_todo_list
API_URL=http://localhost:3000/api/todos
npm install mongoose
//IMPORT MONGOOSE
import mongoose, { Model } from "mongoose"
// CONNECTING TO MONGOOSE (Get Database Url from .env.local)
const { DATABASE_URL } = process.env
// connection function
export const connect = async () => {
const conn = await mongoose
.connect(DATABASE_URL as string)
.catch(err => console.log(err))
console.log("Mongoose Connection Established")
// OUR TODO SCHEMA
const TodoSchema = new mongoose.Schema({
item: String,
completed: Boolean,
})
// OUR TODO MODEL
const Todo = mongoose.models.Todo || mongoose.model("Todo", TodoSchema)
return { conn, Todo }
}
/todos/
this will be handled with this file... /pages/api/todos/index.ts
(index.ts will always serve a route following the folder name)/todos/:id
this will be handled with this file... /pages/api/todos/[id].ts
(the []
denote a URL param in next)// Interface to defining our object of response functions
export interface ResponseFuncs {
GET?: Function
POST?: Function
PUT?: Function
DELETE?: Function
}
// Interface to define our Todo model on the frontend
export interface Todo {
_id?: number
item: string
completed: boolean
}
import { NextApiRequest, NextApiResponse } from "next"
import { connect } from "../../../utils/connection"
import { ResponseFuncs } from "../../../utils/types"
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
//capture request method, we type it as a key of ResponseFunc to reduce typing later
const method: keyof ResponseFuncs = req.method as keyof ResponseFuncs
//function for catch errors
const catcher = (error: Error) => res.status(400).json({ error })
// Potential Responses
const handleCase: ResponseFuncs = {
// RESPONSE FOR GET REQUESTS
GET: async (req: NextApiRequest, res: NextApiResponse) => {
const { Todo } = await connect() // connect to database
res.json(await Todo.find({}).catch(catcher))
},
// RESPONSE POST REQUESTS
POST: async (req: NextApiRequest, res: NextApiResponse) => {
const { Todo } = await connect() // connect to database
res.json(await Todo.create(req.body).catch(catcher))
},
}
// Check if there is a response for the particular method, if so invoke it, if not response with an error
const response = handleCase[method]
if (response) response(req, res)
else res.status(400).json({ error: "No Response for This Request" })
}
export default handler
OverwriteModelError: Cannot overwrite 'Todo' model once compiled.
in your terminal it's cause in development mode it's doing hot replacement which means the Todo model persists but it tries to create it everytime the dev server resets so it gets mad your trying to make a duplicate. This shouldn't be the case in production (when we run npm run build
then run it with npm run start
).import { NextApiRequest, NextApiResponse } from "next"
import { connect } from "../../../utils/connection"
import { ResponseFuncs } from "../../../utils/types"
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
//capture request method, we type it as a key of ResponseFunc to reduce typing later
const method: keyof ResponseFuncs = req.method as keyof ResponseFuncs
//function for catch errors
const catcher = (error: Error) => res.status(400).json({ error })
// GRAB ID FROM req.query (where next stores params)
const id: string = req.query.id as string
// Potential Responses for /todos/:id
const handleCase: ResponseFuncs = {
// RESPONSE FOR GET REQUESTS
GET: async (req: NextApiRequest, res: NextApiResponse) => {
const { Todo } = await connect() // connect to database
res.json(await Todo.findById(id).catch(catcher))
},
// RESPONSE PUT REQUESTS
PUT: async (req: NextApiRequest, res: NextApiResponse) => {
const { Todo } = await connect() // connect to database
res.json(
await Todo.findByIdAndUpdate(id, req.body, { new: true }).catch(catcher)
)
},
// RESPONSE FOR DELETE REQUESTS
DELETE: async (req: NextApiRequest, res: NextApiResponse) => {
const { Todo } = await connect() // connect to database
res.json(await Todo.findByIdAndRemove(id).catch(catcher))
},
}
// Check if there is a response for the particular method, if so invoke it, if not response with an error
const response = handleCase[method]
if (response) response(req, res)
else res.status(400).json({ error: "No Response for This Request" })
}
export default handler
import { Todo } from "../utils/types"
import Link from "next/link"
// Define the components props
interface IndexProps {
todos: Array<Todo>
}
// define the page component
function Index(props: IndexProps) {
const { todos } = props
return (
<div>
<h1>My Todo List</h1>
<h2>Click On Todo to see it individually</h2>
{/* MAPPING OVER THE TODOS */}
{todos.map(t => (
<div key={t._id}>
<Link href={`/todos/${t._id}`}>
<h3 style={{ cursor: "pointer" }}>
{t.item} - {t.completed ? "completed" : "incomplete"}
</h3>
</Link>
</div>
))}
</div>
)
}
// GET PROPS FOR SERVER SIDE RENDERING
export async function getServerSideProps() {
// get todo data from API
const res = await fetch(process.env.API_URL as string)
const todos = await res.json()
// return props
return {
props: { todos },
}
}
export default Index
/todos/:id
to create this page we need to create a file called /pages/todos/[id].tsx
/pages/todos/[id].tsx
import { Todo } from "../../utils/types"
import { useRouter } from "next/router"
import { useState } from "react"
// Define Prop Interface
interface ShowProps {
todo: Todo
url: string
}
// Define Component
function Show(props: ShowProps) {
// get the next router, so we can use router.push later
const router = useRouter()
// set the todo as state for modification
const [todo, setTodo] = useState<Todo>(props.todo)
// function to complete a todo
const handleComplete = async () => {
if (!todo.completed) {
// make copy of todo with completed set to true
const newTodo: Todo = { ...todo, completed: true }
// make api call to change completed in database
await fetch(props.url + "/" + todo._id, {
method: "put",
headers: {
"Content-Type": "application/json",
},
// send copy of todo with property
body: JSON.stringify(newTodo),
})
// once data is updated update state so ui matches without needed to refresh
setTodo(newTodo)
}
// if completed is already true this function won't do anything
}
// function for handling clicking the delete button
const handleDelete = async () => {
await fetch(props.url + "/" + todo._id, {
method: "delete",
})
//push user back to main page after deleting
router.push("/")
}
//return JSX
return (
<div>
<h1>{todo.item}</h1>
<h2>{todo.completed ? "completed" : "incomplete"}</h2>
<button onClick={handleComplete}>Complete</button>
<button onClick={handleDelete}>Delete</button>
<button
onClick={() => {
router.push("/")
}}
>
Go Back
</button>
</div>
)
}
// Define Server Side Props
export async function getServerSideProps(context: any) {
// fetch the todo, the param was received via context.query.id
const res = await fetch(process.env.API_URL + "/" + context.query.id)
const todo = await res.json()
//return the serverSideProps the todo and the url from out env variables for frontend api calls
return { props: { todo, url: process.env.API_URL } }
}
// export component
export default Show
/pages/index.tsx
let's add button to take us to a "Create Todo" page!<h1>My Todo List</h1>
<h2>Click On Todo to see it individually</h2>
<Link href="/todos/create"><button>Create a New Todo</button></Link>
/pages/todos/create.txt
getStaticPaths: allows you define an array of urls, typically used for dynamic routes like [id].tsx
to define all the possible urls and then pre-generate each one.
getStaticProps: allows you to fetch data from other sources and pass it as props at build time before the page is pre-rendered. This request will not occur when the user accesses the page like getServerSideProps or plain frontend fetch requests.
/pages/todos/create.tsx
import { useRouter } from "next/router"
import { FormEvent, FormEventHandler, useRef } from "react"
import { Todo } from "../../utils/types"
// Define props
interface CreateProps {
url: string
}
// Define Component
function Create(props: CreateProps) {
// get the next route
const router = useRouter()
// since there is just one input we will use a uncontrolled form
const item = useRef<HTMLInputElement>(null)
// Function to create new todo
const handleSubmit: FormEventHandler<HTMLFormElement> = async event => {
event.preventDefault()
// construct new todo, create variable, check it item.current is not null to pass type checks
let todo: Todo = { item: "", completed: false }
if (null !== item.current) {
todo = { item: item.current.value, completed: false }
}
// Make the API request
await fetch(props.url, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(todo),
})
// after api request, push back to main page
router.push("/")
}
return (
<div>
<h1>Create a New Todo</h1>
<form onSubmit={handleSubmit}>
<input type="text" ref={item}></input>
<input type="submit" value="create todo"></input>
</form>
</div>
)
}
// export getStaticProps to provie API_URL to component
export async function getStaticProps(context: any) {
return {
props: {
url: process.env.API_URL,
},
}
}
// export component
export default Create
npm run build
builds out the application rendering all static pagesnpm run start
will start the application that npm run build created