Building A Simple CRUD API With Next.Js 13

Hòa Nguyễn Coder - Oct 4 '23 - - Dev Community

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

Building A Simple CRUD API With Next.Js 13 -  hoanguyenit.com

  • 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
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode
  • 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)

}
Enter fullscreen mode Exit fullscreen mode

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)

}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode
  • 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
  )
}

Enter fullscreen mode Exit fullscreen mode
  • 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 />
  )
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Demo:

Building A Simple CRUD API With Next.Js 13 - hoanguyenit.com

Building A Simple CRUD API With Next.Js 13 - hoanguyenit.com

Building A Simple CRUD API With Next.Js 13 - hoanguyenit.com

Building A Simple CRUD API With Next.Js 13 - hoanguyenit.com
The Article : Building A Simple CRUD API With Next.Js 13

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player