Next.js 14 got some cool stuff like the app router which makes it very different of creating API routes. Although you can use the same old convention but due to the new Server actions, this is going to change forever. This feature was originally introduced from the Next.js 13 and was introduces as an experimental feature.
Table of Contents
Writing APIs with Page Router (OLD Method)
Before this app router every file in the pages
directory was a route itself. Next.js supports this convention since version 9.4. Which helps to adopt very own style of managing routes and components. So, for example you can have a folder structure like follows.
src
├── components
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── ...
├── pages
│ ├── index.tsx
│ ├── about.tsx
│ ├── api
│ │ └── hello.ts
│ └── ...
├── styles
│ ├── global.css
│ └── ...
└── utils
└── ...
Code language: CSS (css)
Here is the way of writing API in old style.
export default function handler(req, res) {
//...
}
Code language: JavaScript (javascript)
In the above code, it may seem similar to Node.js style. But it have some more features. For example, the req object which is the Request object and the res
Response object have options like
- req.body
- req.query
- req.cookies
- res.status
- res.json
- res.redirect
If you were developing api’s in TypeScript this would be something like this
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'Hello Abdul Rehman!' })
}
Code language: TypeScript (typescript)
OLD Style GET/POST Requests
In the older versions, to differentiate the GET/POST requests what we need to do is to check the req.method
property. This property returns a string which we can check for the HTTP method. This could be such as GET, POST
, PUT
, DELETE
, etc. Here is how we use to do it.
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
// Handle GET request
res.status(200).json({ message: 'Hello from Next.js!' });
} else if (req.method === 'POST') {
// Handle POST request
// You can access the request body as req.body
res.status(201).json({ message: 'Data received!' });
} else {
// Handle other methods
res.status(405).json({ message: 'Method not allowed' });
}
}
Code language: TypeScript (typescript)
Or we can use some package like next-connect
which make EXPRESS
style request. This will be like as follows.
// pages/api/hello.js
import nc from 'next-connect';
const handler = nc()
.get((req, res) => {
// Handle GET request
res.status(200).json({ message: 'Hello from Next.js!' });
})
.post((req, res) => {
// Handle POST request
// You can access the request body as req.body
res.status(201).json({ message: 'Data received!' });
})
.all((req, res) => {
// Handle other methods
res.status(405).json({ message: 'Method not allowed' });
});
export default handler;
Code language: JavaScript (javascript)
New App Router in Next.js 14
According to the Next.js 14 Release blog post here is what it says.
Here you can see that the api routes are under the api
folder inside the pages
folder. But with the help of app router, the routes are being changed. The app directory is a new feature introduced in Next.js 13. Now there is a new directory inside your `src
` directory. This new directory is called app
directory. Inside this app
directory you create new folders. Every folder itself is a new route.
Inside this folder you create a page.tsx
file or you can create page.jsx
file which will be rendered when that page will be accessed. You can still create a pages and API directory as well. Although all the pages are SSR which means they are rendered on Server Side, unless they are specifically told to be rendered on Client Side by specifying the 'use client'
directive on the top of the file. This directive tells that that specific component would be rendered on client’s browser. With this new app router you can create following type of routing files.
layout | .js .jsx .tsx | Layout |
page | .js .jsx .tsx | Page |
loading | .js .jsx .tsx | Loading UI |
not-found | .js .jsx .tsx | Not found UI |
error | .js .jsx .tsx | Error UI |
global-error | .js .jsx .tsx | Global error UI |
route | .js .ts | API endpoint |
template | .js .jsx .tsx | Re-rendered layout |
default | .js .jsx .tsx | Parallel route fallback page |
As you can see you can create a route.ts file for your api end point here. You can create for a seperate page a seperate route.ts file which will make a seperate end point for that route. For example, if you create a route.ts file under the hello
directory inside the app
directory. You will be able to access that route with /hello
How creating API Route in Next.js 14 with App Router is different from previous?
Now that you have idea of how the routes were written in previous version with the page router, here is how they are different with app router. With the new version of Next.js 14 the app routers use route.js or route.ts file and we have to write the API with either route file or we can use the reuseable server actions. First of all, let’s see how writing API with App Router which is built on top of React Server Components, is different from Page Router? Here is the quick summary.
- Now the API are not under the
pages/api
directory, instead the api end points are of folder name inside theapp
directory. You can place theapi
folder inside theapp
folder. Inside that folder you can place further folders or if you only need one end point you can write the route.ts file just under the/api
directory inside the app folder. - Now the API routes are server components. Which mean they are just normal React functions returning React Elements. So while returning the response instead of just writing it to
res
object, you have to return a React Element. - API Routes now could be written in new Server Actions which could be reused in multiple end points and could be called directly in client components.
How to create API Routes with App Router in Next.js 14?
With the new changes in the next.js 14 and app router, now each api end point is a route
file. Which could be of .js or .ts extention. This will be treated as APIend point. Which is now usual way of creating API in the new Next.js 14. This is the following image from the official documentation page of the next.js 14.
Here you can clearly see that if you create an api folder inside your app folder. You now have to create a route.js or route.ts file to make an API end point. Which you can fetch('/api')
later inside your components.
Now you have to write separate GET and POST methods. Also, you have to return proper element from the API route. Here is the quick example code for writing the GET and POST API end point functions with new APP Router.
GET Route function
import type {NextApiRequest, NextApiResponse} from 'next'
export async function GET (request:NextApiRequest){
return new NextResponse(JSON.stringify({ message: "Welcome John Doe" }), {
status: 200,
});
}
Code language: JavaScript (javascript)
Using POST Method
import type {NextApiRequest, NextApiResponse} from 'next'
export async function POST(request: NextRequest) {
const { username}: MyData = await request.json();
if (!username) {
return new NextResponse(
JSON.stringify({ name: "Please provide something to search for" }),
{ status: 400 }
);
}
return new NextResponse(JSON.stringify({ answer: "John Doe" }), {
status: 200,
});
}
Code language: JavaScript (javascript)
Next.js 14 Server actions instead of API
As mentioned above the Next.js 14 introduced the “Server Actions”. With this thing introduced api calling is being different then it was before. Also, as I had mentioned above that every page and component is server rendered by default so you cannot use react hooks on it. Which means if you want to use even the simplest React.useState hook, you have to mention that your component is client rendered. Which you can simply state by mentioning 'use client';
on top of your component. Which we had done in one of our previous blog post, when we were creating a simple nav bar in next.js 14.
Server Actions are reuseable, which means you can create an actions.ts
file and inside that file every written server action could be imported into many numbers of client components and directory could be used for form submissions. So, with the help of this, now you do not need to write separate API end points for form submissions. Instead, all of these could be replaced with the Next.js 14 based Server Actions. This is the example server actions which use the sql
interactions directly inside the server action functions. This code is taken from this GitHub repository and it simplify things with best example.
import { sql } from '@vercel/postgres';
export async function fetchRevenue() {
// Add noStore() here prevent the response from being cached.
// This is equivalent to in fetch(..., {cache: 'no-store'}).
try {
// Artificially delay a response for demo purposes.
// Don't do this in production :)
// console.log('Fetching revenue data...');
// await new Promise((resolve) => setTimeout(resolve, 3000));
const data = await sql<Revenue>`SELECT * FROM revenue`;
// console.log('Data fetch completed after 3 seconds.');
return data.rows;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch revenue data.');
}
}
export async function fetchLatestInvoices() {
try {
const data = await sql<LatestInvoiceRaw>`
SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 5`;
const latestInvoices = data.rows.map((invoice) => ({
...invoice,
amount: formatCurrency(invoice.amount),
}));
return latestInvoices;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch the latest invoices.');
}
}
export async function fetchInvoicesPages(query: string) {
try {
const count = await sql`SELECT COUNT(*)
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE
customers.name ILIKE ${`%${query}%`} OR
customers.email ILIKE ${`%${query}%`} OR
invoices.amount::text ILIKE ${`%${query}%`} OR
invoices.date::text ILIKE ${`%${query}%`} OR
invoices.status ILIKE ${`%${query}%`}
`;
const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE);
return totalPages;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch total number of invoices.');
}
}
export async function fetchInvoiceById(id: string) {
try {
const data = await sql<InvoiceForm>`
SELECT
invoices.id,
invoices.customer_id,
invoices.amount,
invoices.status
FROM invoices
WHERE invoices.id = ${id};
`;
const invoice = data.rows.map((invoice) => ({
...invoice,
// Convert amount from cents to dollars
amount: invoice.amount / 100,
}));
return invoice[0];
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch invoice.');
}
}
Code language: TypeScript (typescript)