⚔️ Code Conqueror

🗃️ Using a database with Next.js

Nov 01, 2019

Next.js does not have a built in database, but there are simple ways to get databases connected to Next.js through connectors. We'll show some best practices for how to connect a database to Next.js in this article.

Next.js does not have a built in database

Next.js is a solution for building full stack Node / React applications. It’s designed to be compatible with serverless environments, and as such does not preserve any state inside of the service. In order to preserve some state, such as user information, you’ll want to use a database and connect it to the functions that run in Next.js.

Which database to choose, and how to get it set up

There are many good choices for databases and which one to choose will depend on your specific requirements. For example purposes well choose MongoDB because it’s node client is powerful, and it's straightforward to go to production for free with MongoDB Atlas.

For development purposes we’ll want to have Mongo running locally. To do so, we’ll use Docker to spin out an instance of MongoDB (this would also make deploying mongo to your own server in the case you don’t want to use a database as a service like mongo Atlas). To get started create a project directory and add a new

docker-compose.yml
configuration eg:

// data/docker-compose.yml
version: '3'
services:
mongo:
image: mongo:3.6
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: username
MONGO_INITDB_ROOT_PASSWORD: password
ports:
- '27017:27017'
volumes:
- mongo:/var/lib/mongo
volumes:
mongo: ~

Just run

docker-compose up -d
in the same directory as the configuration to get up and running. (Note: you should use a better username / password, ideally with environment variables in a real app).

How to integrate a database with Next.js

It would be very poor practice to directly connect your database to the front end of your application. Even in the case of Next.js which calls the

getInitialProps
function on the server, it is called on the client on route changes. So we won’t want to put the connection to Mongo there.

Luckily Next has made it easy to add lambda style (and if you deploy with serverless target, actually lambdas) API handlers that we can use to proxy connections to the database from our front end.

To get started we’ll spin up a simple next js application using create-next-app.

npx create-next-app with-mongo-connection

Once bootstraped, we'll start by adding a new api route that handles creating a user

/pages/api/user/index.js
. To get started with our mongo database we need to connect to it using a Node.js client for which we will use the popular Mongoose. Just
yarn add mongoose
. You can get your Next.js development server started now with
yarn dev
.

We also need to add the users collection and Next.js database to our MongoDB instance. You can do that as a one-off through a UI like MongoDB Compass or the Atlas UI if you are using a hosted instance. Once we have all that set up we are ready to create our first endpoint to add the user

import mongoose from "mongoose";
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
});
export default async (req, res) => {
const connection = await mongoose.createConnection(
"mongodb://localhost:27017/nextjs",
{
useNewUrlParser: true,
bufferCommands: false,
bufferMaxEntries: 0,
useUnifiedTopology: true,
}
);
try {
const User = connection.model("User", UserSchema);
const {
query: { name },
method,
} = req;
switch (method) {
case "POST":
User.create({ name }, (error, user) => {
if (error) {
connection.close();
res.status(500).json({ error });
} else {
res.status(200).json(user);
connection.close();
}
});
break;
default:
res.setHeader("Allow", ["POST"]);
res.status(405).end(`Method ${method} Not Allowed`);
}
} catch (e) {
connection.close();
res.status(500).json({ error: e.message || "something went wrong" });
}
};

We'll get to cleaning this up into re-usable pieces in a second, but let's walk through what's going on here. Firstly, a Next.js API route requires an express style handler be exported as the default export of the module. The first thing we need to do is to connect to the database. We've put in a dummy connection string, you would normally want some kind of authentication that is reflected in that string. We've also added a database named

nextjs
and a collection named
user
via MongoDB Compass. We define a schema for that user collection via Mongoose.

A user in our example has a single field called

name
. There are a number of ways that we could pass that in eg. in the request body or as a url parameter. We opt to pass it as a URL parameter here so the Create User POST endpoint will look like
/api/user?name=Alexander
. We also take the request method out of the request and ensure that it is indeed POST which is all we support on this route.

Finally we try to create a user with the supplied name and return the object JSON if successful. We also need to be sure to close the connection (this is especially important on a serverless environment).

Adding some utility functions and more endpoints

In total we want our API to have the actions:

  • POST
    /api/user
    - create a new user
  • GET
    /api/users
    - list all users
  • GET
    /api/user/:id
    - get a user with the specified id
  • POST
    /api/user/:id
    - update the for the user with the specified id

First we'll abstract some helper functions out of our

/api/user
endpoint:

// api/user/index.js
import mongoose from "mongoose";
import UserSchema from "../../../data/models/User";
const connectToMongo = async () => {
const connection = await mongoose.createConnection(
"mongodb://localhost:27017/nextjs",
{
useNewUrlParser: true,
bufferCommands: false,
bufferMaxEntries: 0,
useUnifiedTopology: true,
}
);
const User = connection.model("User", UserSchema);
return {
connection,
models: {
User,
},
};
};
const apiHandler = (res, method, handlers) => {
if (!Object.keys(handlers).includes(method)) {
res.setHeader("Allow", Object.keys(handlers));
res.status(405).end(`Method ${method} Not Allowed`);
} else {
handlers[method](res);
}
};
const mongoMiddleware = (handler) => async (req, res) => {
const { connection, models } = await connectToMongo();
try {
await handler(req, res, connection, models);
} catch (e) {
connection.close();
res.status(500).json({ error: e.message || "something went wrong" });
}
};
export default mongoMiddleware(async (req, res, connection, models) => {
const {
query: { name },
method,
} = req;
apiHandler(res, method, {
POST: (response) => {
models.User.create({ name }, (error, user) => {
if (error) {
connection.close();
response.status(500).json({ error });
} else {
response.status(200).json(user);
connection.close();
}
});
},
});
});

We'll move those helpers out into

/api/lib
but the basic ideas are:

  • the mongo connection is always the same so extract it
  • we can define an api handler method that will at least validate that the request method is meant to be handled by this lambda and provide a convenient API for creating the different required methods in an object
  • A mongo middleware that just provides us with all the models and the connection for use in the handlers.
  • All remaining logic inside the default export is the business logic of creating the user

Next let's quickly build up our API a bit with a route to get all users:

// api/users.js
import mongoMiddleware from "../../../lib/api/mongo-middleware";
import apiHandler from "../../../lib/api/api-handler";
export default mongoMiddleware(async (req, res, connection, models) => {
const { method } = req;
apiHandler(res, method, {
GET: (response) => {
models.User.find({}, (error, user) => {
if (error) {
connection.close();
response.status(500).json({ error });
} else {
response.status(200).json(user);
connection.close();
}
});
},
});
});

Which looks a lot cleaner than duplicating all the logic around request handling, method checking etc. As a last example let's create an endpoint to do user specific things (eg fetch a user by id, or update the name of a user).

// api/user/[id].js
import mongoMiddleware from "../../../lib/api/mongo-middleware";
import apiHandler from "../../../lib/api/api-handler";
export default mongoMiddleware(async (req, res, connection, models) => {
const {
query: { id, name },
method,
} = req;
apiHandler(res, method, {
GET: (response) => {
models.User.findById(id, (error, user) => {
if (error) {
connection.close();
response.status(500).json({ error });
} else {
response.status(200).json(user);
connection.close();
}
});
},
POST: (response) => {
User.findOneAndUpdate(id, { name }, {}).exec((error, user) => {
if (error) {
connection.close();
response.status(500).json({ error });
} else {
response.status(200).json(user);
connection.close();
}
});
},
});
});

Some test curl commands you can run on the API to validate that your endpoints are working.

curl http://localhost:3000/api/user?name=george -X POST
curl http://localhost:3000/api/user/[an existinguserid]?name=fred -X POST
curl http://localhost:3000/api/users

Give me the code

We've open sourced all of the code for this demo. Please file any suggestions for improving the example as issues there!