CRUD Tutorial Using Next.js (App Router), TypeScript, Prisma, PlanetScale, and TailwindCSS Part 2

alvinIrwanto
7 min readJan 7, 2024

--

Hello guys, here I am after a long time not written any article and continued the tutorial before.

If you haven’t read the part before, here’s the link to go there:

Before that, I want to say sorry to the one who’s waiting for the tutorial and I want to tell you a bit about the reason.

It is actually because I was really busy with my job at my last company. I really get pushed to handle the job myself about the project, and I kinda don’t have much time to write an article, you know, I’m just too tired I think, and just wanna lie in my bed after that hahaha. But you know, now I’m hit a layoff by the company, what an irony and some of the work that I’ve done is not even used by them. I don’t know, it's just kinda sad to me, after all of that, all of my work is just like useless :(.

Yeah, that’s a little story from me, I still feel the pain, but you know, life must go on, and I’m still searching for a new company right now, and while searching, I will continue to make a blog :).

Okay, let’s continue the tutorial. I know there is so much change already now, like the PlanetScale, you have to put your CC number, update from NextJS, etc. But I think it is not that far to continue the tutorial :).

2. The Coding Part!

First of all, to make it easier, we have to create a folder structure like this in the app directory:

As you can see, different with the pages router, in the app router/app directory, we have to create the name of the pages with a folder and create a file with the name page.tsx.

For the backend part, the main folder will be ‘api’ and inside that, you can create a folder again with the endpoint name that you want. Different from the frontend part, in the backend you will create the file inside that folder with route.ts.

a. The Create page (src/app/create/page.tsx)

The one that also has to be mentioned about the app router is when you use the client component, you have to write “use client” on top of the file, otherwise, it will turn into a server component, which can cause an error.

"use client"

import { useRouter } from 'next/navigation'
import React, { useState } from 'react'

const Page = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [isLoading, setIsLoading] = useState<boolean>(false)
const router = useRouter()

const handleSubmit = async (e: any) => {
e.preventDefault()

setIsLoading(true)

// Because this is a client side (because we use 'use client on top'), so we don't have to add http in the api
await fetch('/api/post', {
method: 'POST', // Method put is to create
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title, content
})
}).then((res) => {
console.log(res)
}).catch((e) => {
console.log(e)
})

setIsLoading(false)
router.push('/')
}

return (
<form className='w-[500px] mx-auto pt-20 flex flex-col gap-2' onSubmit={handleSubmit}>
<input type="text" placeholder='Input your title' value={title} onChange={(e) => setTitle(e.target.value)} className='w-full border p-2 rounded-md' />
<textarea rows={10} placeholder='Input your content' value={content} onChange={(e) => setContent(e.target.value)} className='w-full border p-2 rounded-md' />
<button disabled={isLoading}>{isLoading ? 'Loading ...' : 'Submit'}</button>
</form>
)
}

export default Page

b. The Update Page (src/app/update/page.tsx)

"use client"

import { useRouter } from 'next/navigation'
import React, { useEffect, useState } from 'react'

const Page = ({ params }: { params: { id: string } }) => {

// The update page will need an id in a url
const id = params.id
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [isLoading, setIsLoading] = useState<boolean>(false)
const router = useRouter()

const handleSubmit = async (e: any) => {
e.preventDefault()

setIsLoading(true)

// Because this is a client side (because we use 'use client on top'), so we don't have to add http in the api
await fetch('/api/post', {
method: 'PUT', // Method put is to update
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title, content, id
})
}).then((res) => {
console.log(res)
}).catch((e) => {
console.log(e)
})

setIsLoading(false)

router.push('/')
}

useEffect(() => {
getData()
}, [])

const getData = async () => {
const res = await fetch('/api/post/' + id)
const json = await res.json()

if (!json) {
router.push('/404')
return
}

setTitle(json.post.title)
setContent(json.post.content)
}

return (
<form className='w-[500px] mx-auto pt-20 flex flex-col gap-2' onSubmit={handleSubmit}>
<input type="text" placeholder='Input your title' value={title} onChange={(e) => setTitle(e.target.value)} className='w-full border p-2 rounded-md' />
<textarea rows={10} placeholder='Input your content' value={content} onChange={(e) => setContent(e.target.value)} className='w-full border p-2 rounded-md' />
<button disabled={isLoading}>{isLoading ? 'Loading ...' : 'Update'}</button>
</form>
)
}

export default Page

c. The Item Page (src/app/item.tsx)

This page is used to show the note item and it include the action to delete and update.

'use client'

import React from 'react'
import { Post } from '@prisma/client'
import { useRouter } from 'next/navigation'

interface Props {
post: Post
}

export default function Item({ post }: Props) {

const router = useRouter()

const handleDelete = async (id: number) => {
await fetch('/api/post?id=' + id, {
method: 'DELETE'
})

router.refresh()
}

return (
<div className='border-2 border-black p-3 rounded-md'>
<h2 className='mb-2'>ID: {post.id}</h2>
<h1 className='text-xl font-semibold'>{post.title}</h1>
<p>{post.content}</p>

<div className='flex justify-end gap-3 mt-4 text-sm'>
<button className='font-semibold' onClick={() => router.push(`/update/${post.id}`)}>Update</button>
<button className='font-semibold text-red-500' onClick={() => handleDelete(post.id)}>Delete</button>
</div>
</div>
)
}

d. The Main Page File (src/app/page.tsx)

This page shows the items or notes that are already in the database.

import Link from 'next/link'
import React from 'react'
import Item from './item'

const getPosts = async () => {
// Because this is server components, we have to define the URL with http
const res = await fetch(process.env.BASE_URL + '/api/post', { next: { revalidate: 0 } })

// Define the output to json, because if only res, it will return by any type
const json = await res.json()
return json
}

const Page = async () => {

const posts = await getPosts()

return (
<div className='w-[1200px] mx-auto py-20'>
// This will link to the create page
<Link href={"/create"} className='px-3 py-2 bg-zinc-900 hover:bg-zinc-800 rounded-md text-white'>Create</Link>

<div className='grid grid-cols-3 gap-5 mt-8'>
{posts?.posts?.map((post: any, i: number) => (
<Item key={i} post={post} />
)).sort().reverse()}
</div>
</div>
)
}

export default Page

e. Main Route (src/app/api/route.ts)

This is the main route API or the backend that we create for action in the notes above. We will be using the PrismaClient to create an action request to the database. This will provide the CRUD that will be used in the frontend.

import { PrismaClient } from "@prisma/client";
import { NextRequest, NextResponse } from "next/server";

const prisma = new PrismaClient();

// Action to read
export const GET = async (req: NextRequest) => {
const posts = await prisma.post.findMany({});

return NextResponse.json({
posts,
});
};

// Action to create
export const POST = async (req: NextRequest) => {
const { title, content } = await req.json();

const post = await prisma.post.create({
data: {
title,
content,
},
});

return NextResponse.json({
post,
});
};

// Action to delete
export const DELETE = async (req: NextRequest) => {
const url = new URL(req.url).searchParams;
const id = Number(url.get("id")) || 0;

const post = await prisma.post.delete({
where: {
id: id,
},
});

if (!post) {
return NextResponse.json(
{
message: "Error",
},
{
status: 500,
}
);
}

return NextResponse.json({});
};

// Action to update or edit
export const PUT = async (req: NextRequest) => {
const { title, content, id } = await req.json();

const post = await prisma.post.update({
where: {
id: Number(id),
},

data: {
title,
content,
},
});

return NextResponse.json({
post,
});
};

f. Post Route (src/app/api/post/[id]/route.ts)

This is the route that handles the update note page. Because we will use a different page when we edit the note, it will need to get the data also from the note that we want to update, so it will grab the data from the database.

import { PrismaClient } from "@prisma/client";
import { NextRequest, NextResponse } from "next/server";

const prisma = new PrismaClient();

export const GET = async (
req: NextRequest,
context: { params: { id: string } }
) => {
const id = Number(context.params.id || 0);

const post = await prisma.post.findUnique({
where: {
id: id,
},
});

return NextResponse.json({ post });
};

And that’s it! You already create a simple notes app with that 🥳🎉!

I think that’s all, thank you for reading my article and my little story above hahaha

And if you have any questions, feel free to ask me in the comment, THANK YOUU!

--

--