Now I will make an example about CRUD ( create, read, update, delete ) in NextJS 13 . Share with everyone how to set up routes in NextJS 13, so we can configure paths to create, read, and edit in the application. Here I use the latest version of NextJS 13. For me, I already have a BackEnd, so in this article I will only do the frontend
- app/libs/index.ts : build the libraries you want
- app/types/index.ts : build the interfaces
- api /posts/route.ts : GET (Get a list of all posts), POST (Add a post)
- api/posts/[id]/route.ts : GET : get post via ID PUT : update post from ID DELETE : delete post from ID
- app/post/page.tsx : Display list of posts
- app/post/create/page.tsx : Form to add posts
- app/post/edit/[id]/page.tsx : Form to edit posts from ID
- app/post/read/[id]/page.tsx : Form to display posts from ID
- app/components/Header.ts : design header interface
- app/components/Post.ts : display post data
- app/layout.tsx : project layout interface
- app/page.tsx : home page interface
Demo:
Github : Building A Simple CRUD API With Next.Js 13
Okay let's start building a project
npx create-next-app@latest
If you have not seen the article on creating a NextJS project, please review this article: Create A Project With Next.Js
- app/libs/index.ts : The code below, we handle API requests
export const fetcher = (url: string) => fetch(url).then((res) => res.json());
- app/types/index.ts : set the properties of a Model, using the interface in typescript , need to configure properties of a certain data type
export interface UserModel{
id:number,
name:string,
}
export interface PostModel{
id:number,
title:string,
keyword:string,
des:string,
slug:string,
image:string,
publish:number,
content:string,
created_at:string
user:UserModel,
deletePost:(id: number)=> void;
}
export interface PostAddModel{
title:string,
content:string
}
- api/posts/route.ts : We need to build a route, to request Api, here we need to install 2 methods ( GET , POST )
import { NextRequest, NextResponse } from 'next/server'
export async function GET() {
const res = await fetch(process.env.PATH_URL_BACKEND+'/api/posts', {
headers: {
'Content-Type': 'application/json',
},
})
const result = await res.json()
return NextResponse.json({ result })
}
export async function POST(request: NextRequest) {
const body = await request.json()
const res = await fetch(process.env.PATH_URL_BACKEND+'/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await res.json();
return NextResponse.json(data)
}
process.env.PATH_URL_BACKEND : is the path to your BackEnd address , you create an .env file and use configuration variables for the project.
- api/posts/[id]/route.ts : In this route we use methods such as ( GET , PUT , DELETE ), as I said in the above section GET: used to get posts by ID PUT : update post from ID DELETE : delete post from ID
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request : NextRequest,{ params }: { params: { id: number } }) {
const res = await fetch(process.env.PATH_URL_BACKEND+`/api/posts/${params.id}`, {
next: { revalidate: 10 } ,
headers: {
'Content-Type': 'application/json',
},
})
const result = await res.json()
return NextResponse.json(result)
}
export async function PUT(request: NextRequest,{ params }: { params: { id: number } }) {
const body = await request.json()
const res = await fetch(process.env.PATH_URL_BACKEND+`/api/posts/${params.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await res.json();
return NextResponse.json(data)
}
export async function DELETE(request: NextRequest,{ params }: { params: { id: number } }) {
const res = await fetch(process.env.PATH_URL_BACKEND+`/api/posts/${params.id}`, {
next: { revalidate: 10 },
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
const data = await res.json();
return NextResponse.json(data)
}
You can look at the code above, I use next: { revalidate: 10 } , it is used to save data memory within 10 seconds, depending on your application, configure it.
- app/post/page.tsx : Displays a list of posts for users to see
"use client";
import React,{useEffect, useState} from "react";
import useSWR from "swr";
import { fetcher } from "../libs";
import Post from "../components/Post";
import { PostModel } from "../types";
import Link from "next/link";
export default function Posts() {
const [posts,setPosts] = useState<PostModel[]>([]);
const { data, error, isLoading } = useSWR<any>(`/api/posts`, fetcher);
useEffect(()=>{
if(data && data.result.data)
{
console.log(data.result.data);
setPosts(data.result.data);
}
},[data,isLoading]);
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
let delete_Post : PostModel['deletePost']= async (id:number) => {
const res = await fetch(`/api/posts/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
});
const content = await res.json();
if(content.success>0)
{
setPosts(posts?.filter((post:PostModel)=>{ return post.id !== id }));
}
}
return (
<div className="w-full max-w-7xl m-auto">
<table className="w-full border-collapse border border-slate-400">
<caption className="caption-top py-5 font-bold text-green-500 text-2xl">
List Posts - Counter :
<span className="text-red-500 font-bold">{ posts?.length}</span>
</caption>
<thead>
<tr className="text-center">
<th className="border border-slate-300">ID</th>
<th className="border border-slate-300">Title</th>
<th className="border border-slate-300">Hide</th>
<th className="border border-slate-300">Created at</th>
<th className="border border-slate-300">Modify</th>
</tr>
</thead>
<tbody>
<tr>
<td colSpan={5}>
<Link href={`/post/create`} className="bg-green-500 p-2 inline-block text-white">Create</Link>
</td>
</tr>
{
posts && posts.map((item : PostModel)=><Post key={item.id} {...item} deletePost = {delete_Post} />)
}
</tbody>
</table>
</div>
);
}
There are many things inside the above code that I shared with everyone in the previous article such as: SWR
If you haven't seen it yet, please review it here: Create A Example Handling Data Fetching With SWR In NextJS
Look at this code, I created a function to catch the event of deleting a post
let delete_Post : PostModel['deletePost']= async (id:number) => {
const res = await fetch(`/api/posts/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
});
const content = await res.json();
if(content.success>0)
{
setPosts(posts?.filter((post:PostModel)=>{ return post.id !== id }));
}
}
----------
//chèn function đó qua component để bắt sự kiện click delete
posts && posts.map((item : PostModel)=><Post key={item.id} {...item} deletePost = {delete_Post} />
- app/components/Post.ts : component displays posts and handles click events to delete posts
import React from 'react'
import { PostModel } from '../types'
import Link from 'next/link'
export default function Post(params: PostModel) {
return (
<tr>
<td className='w-10 border border-slate-300 text-center'>{params.id}</td>
<td className='border border-slate-300'>{params.title}</td>
<td className='border border-slate-300 text-center'>{params.publish>0?'open':'hide'}</td>
<td className='border border-slate-300 text-center'>{params.created_at}</td>
<td className='w-52 border border-slate-300'>
<span onClick={()=>params.deletePost(params.id)} className='bg-red-500 p-2 inline-block text-white text-sm'>Delete</span>
<Link href={`/post/edit/${params.id}`} className='bg-yellow-500 p-2 inline-block ml-3 text-white text-sm'>Edit</Link>
<Link href={`/post/read/${params.id}`} className='bg-yellow-500 p-2 inline-block ml-3 text-white text-sm'>View</Link>
</td>
</tr>
)
}
Catch click event to delete post: params.deletePost(params.id)
- app/post/create/page.tsx : Create a form to enter information to add posts, the code below uses useState to save data, in general it is the same as React. So I will skip this explanation
"use client"
import React, {useState } from 'react'
import { useRouter } from 'next/navigation'
export default function PostCreate() {
const router = useRouter()
const [title, setTitle] =useState<string>('');
const [body, setBody] = useState<string>('');
const addPost = async (e: any) => {
e.preventDefault()
if (title!="" && body!="") {
const formData = {
title: title,
content: body
}
const add = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const content = await add.json();
if(content.success>0)
{
router.push('/post');
}
}
};
return (
<form className='w-full' onSubmit={addPost}>
<span className='font-bold text-yellow-500 py-2 block underline text-2xl'>Form Add</span>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Title</label>
<input type='text' name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' onChange={(e:any)=>setTitle(e.target.value)}/>
</div>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Content</label>
<textarea name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' onChange={(e:any)=>setBody(e.target.value)} />
</div>
<div className='w-full py-2'>
<button className="w-20 p-2 text-white border-gray-200 border-[1px] rounded-sm bg-green-400">Submit</button>
</div>
</form>
)
}
- app/post/edit/[id]/page.tsx : Edit the post, by getting the ID of the post, request to /api/posts/edit/[id]/route.ts to get the data to edit fix
"use client"
import React, {useState,useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { fetcher } from '@/app/libs'
import useSWR from 'swr'
export default function PostEdit({params} :{params:{id:number}}) {
const router = useRouter()
const {data : post,isLoading, error} = useSWR(`/api/posts/${params.id}`,fetcher)
const [title, setTitle] =useState<string>('');
const [body, setBody] = useState<string>('');
useEffect(()=>{
if(post){
setTitle(post.result.title)
setBody(post.result.content)
}
},[post, isLoading])
const updatePost = async (e: any) => {
e.preventDefault()
if (title!="" && body!="") {
const formData = {
title: title,
content: body
}
const res = await fetch(`/api/posts/${params.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const content = await res.json();
if(content.success>0)
{
router.push('/post');
}
}
};
if(isLoading) return <div><span>Loading...</span></div>
if (!post) return null;
return (
<form className='w-full' onSubmit={updatePost}>
<span className='font-bold text-yellow-500 py-2 block underline text-2xl'>Form Add</span>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Title</label>
<input type='text' name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' value={title} onChange={(e:any)=>setTitle(e.target.value)}/>
</div>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Content</label>
<textarea name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' value={body} onChange={(e:any)=>setBody(e.target.value)} />
</div>
<div className='w-full py-2'>
<button className="w-20 p-2 text-white border-gray-200 border-[1px] rounded-sm bg-green-400">Submit</button>
</div>
</form>
)
}
- app/post/read/[id]/page.tsx : Similar to Edit, but in this route we only need to display information for the user to see
'use client'
import { fetcher } from '@/app/libs'
import useSWR from 'swr'
export default function Detail({params}: {params:{id :number}}) {
const {data: post, isLoading, error} = useSWR(`/api/posts/${params.id}`,fetcher)
if(isLoading) return <div><span>Loading...</span></div>
if (!post) return null;
return (
<div className='w-full'>
<h2 className='text-center font-bold text-3xl py-3'>{post.result.title}</h2>
<div className='w-full max-w-4xl m-auto border-[1px] p-3 border-gray-500 rounded-md'>
<p dangerouslySetInnerHTML={{ __html: post.result.content}}></p>
</div>
</div>
)
}
- app/page.tsx : import component /app/post/page.tsx , to display the main screen of the home page
import Posts from './post/page'
export default function Home() {
return (
<Posts />
)
}
- app/layout.tsx : application layout
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Header from './components/Header'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<Header />
<div className='w-full max-w-7xl mt-4 m-auto'>
{children}
</div>
</body>
</html>
)
}
Demo:
The Article : Building A Simple CRUD API With Next.Js 13
Top comments (6)
github??
You can see here : Building A Simple CRUD API With Next.Js 13
I'm getting 500 (internal server error), can you check?
Thanks
The above code is FrontEnd, you need a server backend, after then you can request api
You're trying to fetch data on the client in nextjs app router??
You should avoid that, instead you should fetch data on the server components, i mean don't call fetch api to the components that has "use client" you're loosing the server components advantages.
is there a github for the backend?