Route Handlers in Next.js 13

In this tutorial, we’ll utilize Route Handlers feature of Next.js 13. Route Handlers are typically used to define API routes / endpoints (also known as serverless functions).

This tutorial builds upon the instructions given in the previous tutorial. So, first go through the previous tutorial and follow all steps given there to setup Prisma and SQLite within Next.js 13 app, then continue from here.

Having setup our Next.js 13 app with Prisma and SQLite, enter following command in parent directory (my-app) to create app/api/posts folder:

mkdir app/api/posts -p

Notice the /api/ folder – that’s where all the route handlers go. In Next.js 13, the API routes are defined through a special file called route.js (routes.ts in case TypeScript) instead of page.js. You can either have route.js or page.js within a single route (folder). So, let’s create a route.ts file inside app/api/posts:

touch app/api/posts/route.ts

Then insert following code inside app/api/posts/route.ts:

import { PrismaClient } from '@prisma/client'
import { NextResponse } from "next/server"

const prisma = new PrismaClient()

export async function GET() {
  const posts = await prisma.post.findMany()
  return NextResponse.json(posts)
}

export async function POST(request: Request) {

    const json = await request.json();
 //   const data = await request.formData()
    const post = await prisma.post.create({
      data: json
    })

//return NextResponse.redirect('http://localhost:3000/posts', {status: 302});
return NextResponse.json({ post })
}

In order to handle dynamic routes (GET, PATCH, DELETE) which are dependent upon certain id or slug, create a dynamic route by creating following folder:

mkdir app/api/posts/'[id]'

Then, create route.ts file inside app/api/posts/[id] folder by executing:

touch app/api/posts/'[id]'/route.ts

Now, insert following code inside app/api/posts/[id]/route.ts:

import { PrismaClient } from '@prisma/client'
import { NextResponse } from "next/server"

const prisma = new PrismaClient()

export async function GET(
  request: Request,
  { params }: { params: { id } }
) {
  const id = parseInt(params.id);
  const post = await prisma.post.findUnique({
    where: {
      id,
    },
  })

  return NextResponse.json(post)
}

export async function PATCH(
  request: Request,
  { params }: { params: { id } }
) {
  const id = parseInt(params.id)

//  const data = await request.formData()
  const json = await request.json()
    
  const updated_post = await prisma.post.update({
    where: { id },
    data: json,
  })

  if (!updated_post) {
    return new NextResponse("No post with ID found", { status: 404 })
  }

  return NextResponse.json(updated_post)
}

export async function DELETE(
  request: Request,
  { params }: { params: { id } }
) {
  try {
    const id = parseInt(params.id);
    await prisma.post.delete({
      where: { id },
    })

    return new NextResponse(null, { status: 204 })
  } catch (error: any) {
    if (error.code === "P2025") {
      return new NextResponse("No post with ID found", { status: 404 })
    }

    return new NextResponse(error.message, { status: 500 })
  }
}

We’ve successfully defined API routes / endpoints within our Next.js app. Let’s consume these APIs in old-school way.

Create folder app/posts:

mkdir app/posts

Then create file app/posts/page.tsx:

touch app/posts/page.tsx

And then insert following code into app/posts/page.tsx:

'use client'

import { useState, useEffect } from "react"
import Link from 'next/link'
import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()
  const [data, setData] = useState([])

  useEffect(() => {
    fetch('http://localhost:3000/api/posts', {cache: 'no-store'})
      .then((res) => res.json())
      .then((dat) => {
        setData(dat)
      })
  }, [])


 const handleClick = (id) => {
  fetch(`http://localhost:3000/api/posts/${id}`, {method: 'DELETE'})
  .then(location.reload())
 }
  
  return (
    <div className="p-4 m-4">
      <Link href="/posts/create" className="text-gray-700 px-4 py-2 bg-white hover:bg-gray-100 border rounded-full">Create Post</Link>
      <div className="mt-8 relative overflow-x-auto shadow-md sm:rounded-lg">
        <table className="w-full text-sm text-left text-gray-500">
          <thead className="text-gray-700 bg-gray-50">
            <tr>
              <th scope="col" className="px-6 py-3">
                ID
              </th>
              <th scope="col" className="px-6 py-3">
                Title
              </th>
              <th scope="col" className="px-6 py-3">
                Body
              </th>
              <th scope="col" className="px-6 py-3">
                Action
              </th>
            </tr>
          </thead>
          <tbody>
            {data &&
              data.map((item, i) => (
                <tr key={i} className="bg-white border-b">
                  <td scope="row" className="px-6 py-4 font-medium text-gray-900">
                    {item.id}
                  </td>
                  <td scope="row" className="px-6 py-4 font-medium text-gray-900">
                    {item.title}
                  </td>
                  <td scope="row" className="px-6 py-4 font-medium text-gray-900">
                    {item.body}
                  </td>
                  <td scope="row" className="px-6 py-4 font-medium text-gray-900">
                    <Link href={`/posts/edit/${item.id}`} className="text-gray-700 px-4 py-2 mr-2 bg-blue-100 hover:bg-blue-200 border rounded-full">Edit</Link>
                    <button onClick={(e) => handleClick(item.id)} className="text-gray-700 px-4 py-2 bg-red-100 hover:bg-red-200 border rounded-full">Delete</button>
                  </td>
                </tr>
              ))
            }
          </tbody>
        </table>
      </div>
    </div>
  )
}

Now, create a new folder app/posts/create:

mkdir app/posts/create

Then create file app/posts/create/page.tsx:

touch app/posts/create/page.tsx

And then insert following code inside app/posts/create/page.tsx:

'use client'

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

export default function PostAdd() {

  const router = useRouter()
  const [formValues, setFormValues] = useState({title:'',body:''})
  
  const handleChange = (e) => {
    setFormValues({ ...formValues, [e.target.id]: e.target.value });
  }
  
  const submitContact = (event) => {
    event.preventDefault();
    const res = fetch(`/api/posts`, {
      body: JSON.stringify({
        title: formValues.title,
        body: formValues.body,
      }),
      headers: {
        'Content-Type': 'application/json',
      },
      method: 'POST',
      }).then(router.push('/posts'))
  }
  
    return (
        <div>
            <div className="p-8 justify-center items-center h-screen flex">
                <form onSubmit={submitContact} className="flex">
                    <input type="text" name="title" id="title" className="bg-white shadow-inner rounded-l p-2 flex-1 mr-2" value={formValues.title} onChange={handleChange} placeholder="Title"/>
                    <input type="text" name="body" id="body" className="bg-white shadow-inner rounded-l p-2 flex-1 mr-2" value={formValues.body} onChange={handleChange} placeholder="Body"/>
                    <input type="submit" id="submit" value="submit" className="bg-blue-600 hover:bg-blue-700 duration-300 text-white shadow p-2 rounded-lg" />
                </form>
            </div>
        </div>
    )
}

For editing posts, create a folder edit/[postId] inside app/posts/ folder:

mkdir app/posts/edit/'[postId]' -p

Now, create page.tsx inside the above folder:

touch app/posts/edit/'[postId]'/page.tsx

And then insert following code inside app/posts/edit/[postId]/page.tsx:

'use client'

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

export default function PostUpdate({ params }: { params: { postId: string } }) {

  const router = useRouter()
  const [formValues, setFormValues] = useState({id:'',title:'',body:''})

  useEffect(() => {
    fetch('http://localhost:3000/api/posts/'+params.postId)
      .then((res) => res.json())
      .then((data) => {
        setFormValues(data)
      })
  }, [])
  
  const handleChange = (e) => {
    setFormValues({ ...formValues, [e.target.id]: e.target.value });
  }

  const submitContact = (event) => {
    event.preventDefault();
    const res = fetch(`/api/posts/${formValues.id}`, {
      body: JSON.stringify({
        title: formValues.title,
        body: formValues.body,
      }),
      headers: {
        'Content-Type': 'application/json',
      },
      method: 'PATCH',
      }).then(router.push('/posts'))
  }

  return (
    <div>
      <div className="p-8 justify-center items-center h-screen flex">
        <form onSubmit={submitContact} className="">
          <input type="text" name="title" id="title" className="bg-white shadow-inner rounded-l p-2 flex-1 mr-2" value={formValues.title} onChange={handleChange} placeholder="Title"/>
          <input type="text" name="body" id="body" className="bg-white shadow-inner rounded-l p-2 flex-1 mr-2" value={formValues.body} onChange={handleChange} placeholder="Body"/>
          <input type="submit" id="submit" value="submit" className="bg-blue-600 hover:bg-blue-700 duration-300 text-white shadow p-2 rounded-lg" />
        </form>
      </div>
    </div>
  )
}

Assuming dev server (npm run dev) is already running, enter following URL in the browser:

localhost:3000/posts

(Caution: We needed to test our API routes as quickly as possible; so, there are many things wrong with above client-side components: ideally, we have to have a single form for performing both insert and edit depending upon whether an id has been passed on or not. Also, rendering data-table as in app/posts/page.tsx is not the way it should be, instead data-table should be a separate component and app/posts/page.tsx should be a server-side component embodying other components such as data-table. And then there is that horrendous location.reload() – don’t do that in SPA – that’s the hard refresh for the page – we used it shamelessly just to show refreshed data-table after deleting a record.)

1 thought on “Route Handlers in Next.js 13”

Leave a Comment