Next.js is a terrific choice for developers of any skill level because it is packed with useful features and comes with excellent documentation (along with an introduction course).
When most developers hear about Next.js, the first thing that springs to mind is “frontend web development.” Many people aren’t aware of the API routes functionality, which allows you to write both frontend and backend code in the same file. The API routes feature of Next.js allows developers the flexibility to simply construct lambda functions for their project’s API when paired with a serverless platform like Vercel (which was designed expressly for Next.js) or Netlify.
We’ll use this novel feature to develop a rudimentary example of a real-world API in this lesson.
To begin, use the command npx create-next-app api-routes to create a new project named api-routes.
As part of our Next.js application folder structure, API routes allow us to establish RESTful endpoints. We need to establish a folder named ‘api’ within the page folder, and within that folder, we can define all APIs for our application and add business logic without having to write any additional custom server code or set up any API routes.
Now create a file hello.js in the api folder like below:
1
2
3
4
5
6
export default function handler(req, res) {
res.status(200)
.json({
name: 'Nikhil Kumar Singh'
})
}
Directing to URL: http://localhost:3000/api/hello you will see, confirming our URL works.
Now let’s upgrade our setup. Create a sub/nested folder article.
We may need to collect, submit, or remove data with the click of a button in a business application, for which we may design several sorts of APIs, which we will describe below:
We’ve also built a database folder in which we’ll keep our data for this tutorial, as connecting to a real one would be a separate post. To implement our API, we’ll utilize the file books.js. We’ve built a books folder in the api folder for this purpose.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const books = [
{
id: 1,
title: "Things fall apart",
pages: 209,
language: "English"
},
{
id: 2,
title: "Fairy tails",
pages: 784,
language: "Danish"
},
{
id: 3,
title: "The book of Job",
pages: 176,
language: "Hebrew"
},
{
id: 4,
title: "Pride and Prejudice",
pages: 443,
language: "French"
}
]
Create an index.js file in the books folder; because this is an API route, we’ll export a default handler function that accepts requests and responses as inputs.
1
2
3
4
5
6
7
8
9
index.js
import {
books
} from "../../database/books";
export default function handler(req, res) {
res.status(200)
.json(books)
}
Directing to URL: http://localhost:3000/api/books you will see, confirming our URL works.
Now, as our API works, let’s use this data to display the stored books. We simply made a new page, books:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import {
useState
} from 'react'
function BooksPage() {
const [books, setBooks] = useState([])
const fetchBooks = async () => {
const response = await fetch('/api/books')
const data = await response.json()
console.log(data)
setBooks(data)
}
return (
<>
<div align="center">
<button onClick={fetchBooks}>Get the latest books</button>
</div>
{books.map(book => {
return (
<div align="center" key={book.id}>
{book.id}.<br/>
{"Title: "}{book.title}.<br/>
{"Pages: "} {book.pages}.<br/>
{"Language: "}{book.language} <br/>
<hr/>
</div>
)
})}
</>
)
}
export default BooksPage
We’re using the fetchBooks function to retrieve books from the database, and we’re using useState to alter the state with a single button hit. The following is the result:
Now, in order to showcase the POST APIs, we will allow the user to add books to our database array. To do so, we must first deal with the frontend, since we need the title, pages, and language inputs. We’ve included a new button for submitting new books.
books/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import {
useState
} from 'react'
function BooksPage() {
const [title, setTitle] = useState([])
const [pages, setPages] = useState([])
const [lan, setLan] = useState([])
const [books, setBooks] = useState([])
const fetchBooks = async () => {
const response = await fetch('/api/books')
const data = await response.json()
console.log(data)
setBooks(data)
}
const submitBook = async () => {
const response = await fetch('/api/books', {
method: 'POST',
body: JSON.stringify({
title,
pages,
language: lan
}),
headers: {
'Content-Type': 'application/json'
}
})
const data = await response.json()
console.log(data)
}
return (
<>
<div align="center">
{"Title: "}<input
type='text'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<br/>
{"Pages: "}<input
type='text'
value={pages}
onChange={e => setPages(e.target.value)}
/>
<br/>
{"Language: "}<input
type='text'
value={lan}
onChange={e => setLan(e.target.value)}
/>
<br/>
<button onClick={submitBook}>Submit book</button>
</div>
<br/>
<br/>
<br/>
<div align="center">
<button onClick={fetchBooks}>Get the latest books</button>
</div>
{books.map(book => {
return (
<div align="center" key={book.id}>
{book.id}.<br/>
{"Title: "}{book.title}.<br/>
{"Pages: "} {book.pages}.<br/>
{"Language: "}{book.language} <br/>
<hr/>
</div>
)
})}
</>
)
}
export default BooksPage
In the submitBook function, we are making a POST request to the same API which we defined in the above example, /api/books, using the fetch API. As this is not a GET request we also need to specify a second argument. It is an object where we specify the method as POST and the third argument is the body which is a stringify JSON the book details, as since we are sending JSON data we specify the content type in headers.
Handling the POST request differs from conventional GET requests, as we have to check the incoming request type, in which we do use the request.method rest steps you can see below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {
books
} from "../../../database/books";
export default function handler(req, res) {
if (req.method === 'GET') {
res.status(200)
.json(books)
} else if (req.method === 'POST') {
const title = req.body.title
const pages = req.body.pages
const language = req.body.language
const newBook = {
id: Date.now(),
title,
pages,
language
}
books.push(newBook)
res.status(201)
.json(newBook)
}
}
Now we can add some details about the book. Click submitBook then load the updated list of books using the ‘get the latest books’ button.
To use this API in our example project, each book must have a delete button that can be used to remove a specific book from the database by entering the book Id.
books/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import {
useState
} from 'react'
function BooksPage() {
const [title, setTitle] = useState([])
const [pages, setPages] = useState([])
const [lan, setLan] = useState([])
const [books, setBooks] = useState([])
const deleteBook = async bookId => {
const response = await fetch(`/api/books/{bookId}`, {
method: 'DELETE'
})
const data = await response.json()
console.log(data)
fetchBooks()
}
const fetchBooks = async () => {
const response = await fetch('/api/books')
const data = await response.json()
console.log(data)
setBooks(data)
}
const submitBook = async () => {
const response = await fetch('/api/books', {
method: 'POST',
body: JSON.stringify({
title,
pages,
language: lan
}),
headers: {
'Content-Type': 'application/json'
}
})
const data = await response.json()
console.log(data)
}
return (
<>
<div align="center">
{"Title: "}<input
type='text'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<br/>
{"Pages: "}<input
type='text'
value={pages}
onChange={e => setPages(e.target.value)}
/>
<br/>
{"Language: "}<input
type='text'
value={lan}
onChange={e => setLan(e.target.value)}
/>
<br/>
<button onClick={submitBook}>Submit book</button>
</div>
<br/>
<br/>
<br/>
<div align="center">
<button onClick={fetchBooks}>Get the latest books</button>
</div>
{books.map(book => {
return (
<div align="center" key={book.id}>
{book.id}.<br/>
{"Title: "}{book.title}.<br/>
{"Pages: "} {book.pages}.<br/>
{"Language: "}{book.language} <br/>
<button onClick={() => deleteBook(book.id)}>Delete</button>
<hr/>
</div>
)
})}
</>
)
}
export default BooksPage
DeleteBook is an async function that receives bookId as a parameter and in the function body we make a delete request using the fetch API on the URL /api/books/{bookId}; it is not a GET request. We also need to specify a second argument, it is an object where we specify the method as DELETE, then we update the UI using fetchBooks with the update data.
Because this is a dynamic API, we’ll need to create a separate file to handle these types of queries. Create a new file in the pages/api/books folder called [bookId].js. It’s much the same as before, with the exception that we now have one additional query to deal with.
[bookId].js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {
books
} from '../../../data/books'
export default function handler(req, res) {
const {
bookId
} = req.query
if (req.method === 'GET') {
const book = books.find(book => book.id === parseInt(bookId))
res.status(200)
.json(book)
} else if (req.method === 'DELETE') {
const deletedbook = books.find(
book => book.id === parseInt(bookId)
)
const index = books.findIndex(
book => book.id === parseInt(bookId)
)
books.splice(index, 1)
res.status(200)
.json(deletedbook)
}
}
Now navigate to the page and delete the first book {id = 1}.
We can often handle GET, POST, and DELETE requests using dynamic API routes, but occasionally we have an API where the segments are optional, such as /api/a/b/c, where b,c might be optional, or we could even want a URL to accommodate multiple segments, in which case we utilize catch-all routes.
To catch all routes for pages, such as […params].js, the naming convention is equivalent.
For example
[…params].js
1
2
3
4
5
6
export default function handler(req, res) {
const params = req.query.params
console.log(params)
res.status(200)
.json(params)
}
Output for URL: /api/nikhil/kumar/singh
Built-in middlewares in API routes parse the incoming request (req). These are the middlewares:
req.cookies - An object holding the request’s cookies. This is the default value.
req.query - The query string is stored in this object. This is the default value.
req.body - An object holding the content-type-parsed body, or null if no body was supplied.
Every API route may output a config object, which can be used to override the default configurations, which are as follows:
1 2 3 4 5 6 7
export const config = { api: { bodyParser: { sizeLimit: '1mb', }, }, }
All configs for API routes are available in the API object. BodyParser enables body parsing; if you wish to consume it as a stream, deactivate it:
1 2 3 4 5
export const config = { api: { bodyParser: false, }, }
BodyParser.sizeLimit specifies the maximum size of the parsed body in any bytes-based format, such as:
1 2 3 4 5 6 7
export const config = { api: { bodyParser: { sizeLimit: '250kb', }, }, }
You may also use middleware that is connect compliant.
The cors package, for example, may be used to configure CORS for your API endpoint.
Install cors first:npm i cors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import Cors from 'cors'
const cors = Cors({
methods: ['GET', 'HEAD'],
})
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) {
return reject(result)
}
return resolve(result)
})
})
}
async function handler(req, res) {
await runMiddleware(req, res, cors)
res.json({
message: 'Hey Everyone!'
})
}
export default handler
The server response object (often abbreviated as res) has a collection of Express.js-like helper functions that improve the developer experience and speed up the creation of new API endpoints.
The following are included as helpers:
res.status(code) – Sets the status code. A valid HTTP status code is required.
res.json(body) - Returns a JSON object. A serializable object must be used for the body.
Send the HTTP response with res.send(body). A string, an object, or a buffer can be used as the body.
Redirect to a given path or URL using res.redirect([status,] path). Status must be an HTTP status code that is legitimate. “307” “Temporary redirect” is the default status if it is not given.
You can set the status code of a response when sending it back to the client.
1
2
3
4
5
6
export default function handler(req, res) {
res.status(200)
.json({
message: 'Welcome to Topcoder!'
})
}
You can send a JSON answer back to the client, but it must be a serializable object. In a real-world application, you might wish to inform the client about the request’s status based on the response from the requested endpoint.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default async function handler(req, res) {
try {
const result = await someAsyncOperation()
res.status(200)
.json({
result
})
} catch (err) {
res.status(500)
.json({
error: 'failed to load'
})
}
}
Sending an HTTP response works the same way as when sending a JSON response. The only difference is that the response body can be a string, an object or a buffer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default async function handler(req, res) {
try {
const result = await someAsyncOperation()
res.status(200)
.send({
result
})
} catch (err) {
res.status(500)
.send({
error: 'failed to fetch data'
})
}
}
API routing works similarly to page-based routing. The filename of an API is used to associate it with a route. Every API route exposes a default function, often named handler, which accepts the request and response as inputs and uses the req.method to differentiate between GET and POST. We can utilize dynamic API routes to handle routes with a single argument, and catch-all routes to handle routes with many parameters.