DEV Community

Cover image for Building A Simple CRUD API With Next.Js 13
Hòa Nguyễn Coder
Hòa Nguyễn Coder

Posted on

Building A Simple CRUD API With Next.Js 13

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

Top comments (6)

Collapse
 
surajzzz profile image
surajz

github??

Collapse
 
skipperhoa profile image
Hòa Nguyễn Coder
Collapse
 
surajzzz profile image
surajz • Edited

Image description
I'm getting 500 (internal server error), can you check?
Thanks

Thread Thread
 
skipperhoa profile image
Hòa Nguyễn Coder

The above code is FrontEnd, you need a server backend, after then you can request api

Collapse
 
alkadohs profile image
Alkado heneliko

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.

Collapse
 
aguud profile image
Agus Pranyoto

is there a github for the backend?