Building A Simple CRUD API With Next.Js 13
Hòa Nguyễn Coder
Posted on October 4, 2023
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
Posted on October 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.