iTranslated by AI
Creating a RESTful API with Next.js, MongoDB, and TypeScript
Introduction
With Next.js's unstoppable momentum, this is the first part of a series on Next.js, outlining the steps to create a simple API by integrating with the NoSQL database MongoDB.
I'm sure there are some areas where my understanding is shallow. If you find any such points, I would be grateful if you could teach me.
Next.js & MongoDB (TypeScript) Simple Setup
The official Next.js GitHub repository contains many examples.
This time, we will use the with-typescript folder from these examples as a base.
Setup is very easy; just go to your terminal and run the following command:
yarn create next-app --example with-typescript project-name
Let's check the file structure.
Create an src folder and place the components, interfaces, pages, and utils folders directly under src.
Next, install MongoDB and axios.
yarn add mongodb axios
yarn add -D types/mongodb
Goal of This Article
I want to reproduce the process of replacing the data obtained from utils > sample-data with data created through MongoDB.
Writing the Code
① Set up environment variables
Create a .env.local file in the root directory.
MONGODB_URI= // Your MongoDB connection string
MONGODB_DB= // dbname
② Create mongodb.ts file
Rename the sample-data.ts file within the utils folder to mongodb.ts and rewrite the code as follows:
import { MongoClient, Db } from 'mongodb';
const { MONGODB_URI: uri, MONGODB_DB: dbName } = process.env;
let cachedClient: MongoClient;
let cachedDb: Db;
if (!uri) {
throw new Error(
'Please define the MONGODB_URI environment variable inside .env.local'
);
}
if (!dbName) {
throw new Error(
'Please define the MONGODB_DB environment variable inside .env.local'
);
}
export async function connectToDatabase() {
if (cachedClient && cachedDb) { // Check if cache variables are populated
return { client: cachedClient, db: cachedDb };
}
const client = await MongoClient.connect(uri!, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = await client.db(dbName);
cachedClient = client;
cachedDb = db;
return { client, db };
}
③ Create data in MongoDB
In MongoDB, create a database under Clusters > COLLECTIONS.
Next, press the INSERT DOCUMENT button to create data.
Create multiple data entries.
Move to the editor and type the data to be acquired. (Make changes to User.)
export type User = {
- id: number
+ _id:string
+ id:string
name: string
}
④ Connect to MongoDB
Navigate to src > pages > api > users > index.ts, which is responsible for connecting to MongoDB, and modify the code as follows:
import { NextApiRequest, NextApiResponse } from "next";
import { connectToDatabase } from "../../../utils/mongodb";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
const { method } = req;
switch (method) {
case "GET":
// Get data from mongodb
const { db } = await connectToDatabase();
const data = await db.collection("user").find().toArray(); // Convert to associative array
res.status(200).json(data); // Get data in JSON format
break;
default:
res.setHeader("Allow", ["GET", "PUT"]);
res.status(405).end(`Method ${method} Not Allowed`);
}
} catch (err) {
res.status(500).json({ statusCode: 500, message: err.message });
}
};
export default handler;
The above code was inspired by next.js > examples > api-routes-rest > pages > api > user/[id].js. ↓
Accessing /api/user confirms that data is being retrieved.
We will also make changes to each component.
users/index.ts
-import { GetStaticProps } from 'next'
+import { GetServerSideProps } from "next";
import Link from 'next/link'
import { User } from '../../interfaces'
import { sampleUserData } from '../../utils/sample-data'
import Layout from '../../components/Layout'
import List from '../../components/List'
type Props = {
items: User[]
}
const WithStaticProps = ({ items }: Props) => (
<Layout title="Users List | Next.js + TypeScript Example">
<h1>Users List</h1>
<p>
Example fetching data from inside <code>getStaticProps()</code>.
</p>
<p>You are currently on: /users</p>
<List items={items} />
<p>
<Link href="/">
<a>Go home</a>
</Link>
</p>
</Layout>
)
-export const getStaticProps: GetStaticProps = async () => {
- // Example for including static props in a Next.js function component --page.
- // Don't forget to include the respective types for any props passed -into
- // the component.
- const items: User[] = sampleUserData
- return { props: { items } }
-}
+export const getServerSideProps: GetServerSideProps = async () => {
+ const response = await axios.get<User[]>("http://localhost:3000/api/users");
+ const items = await response.data;
+
+ return { props: { items } };
+};
export default WithStaticProps
SSG (using the getStaticProps function) only fetches data at build time, making it unsuitable for frequently changing data.
Therefore, by converting to SSR (using the getServerSideProps function), data is fetched at build time and also for each request.
The getServerSideProps function can fetch external data and pass the retrieved data as props to the page.
As can be seen in the code, the items prop is passed to the WithStaticProps component.
users/[id].tsx
import { GetStaticProps, GetStaticPaths } from "next";
import { User } from "../../interfaces";
import Layout from "../../components/Layout";
import ListDetail from "../../components/ListDetail";
import axios from "axios";
type Props = {
item?: User;
errors?: string;
};
const StaticPropsDetail = ({ item, errors }: Props) => {
if (errors) {
return (
<Layout title='Error | Next.js + TypeScript Example'>
<p>
<span style={{ color: "red" }}>Error:</span> {errors}
</p>
</Layout>
);
}
return (
<Layout
title={`${
item ? item.name : "User Detail"
} | Next.js + TypeScript Example`}
>
{item && <ListDetail item={item} />}
</Layout>
);
};
export default StaticPropsDetail;
export const getStaticPaths: GetStaticPaths = async () => {
// Get the paths we want to pre-render based on users
const response = await axios.get<User[]>("http://localhost:3000/api/users");
const items = await response.data;
const paths = items.map((user) => ({
params: { id: user.id },
}));
// We'll pre-render only these paths at build time.
// { fallback: false } means other routes should 404.
return { paths, fallback: false };
};
// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries.
export const getStaticProps: GetStaticProps = async ({ params }) => {
try {
const id = params?.id;
const response = await axios.get<User[]>("http://localhost:3000/api/users");
const items = await response.data;
const item = items.find((data) => data.id === id);
// By returning { props: item }, the StaticPropsDetail component
// will receive `item` as a prop at build time
return { props: { item } };
} catch (err) {
return { props: { errors: err.message } };
}
};
Files with names enclosed in square brackets become dynamic pages in Next.js.
And, using the asynchronous function getStaticPaths, it's possible to statically generate pages through dynamic routing. Within this function, you must return a list of possible values for the id.
Finally, implement getStaticProps. In this case, it fetches the necessary data based on the received id. getStaticProps receives params, which includes the id.
With the above changes, the data is now correctly reflected.
The user information is linked to a URL, and a RESTful API has been created!
That's all. Thank you for reading this far!!
Discussion