Next.js and MongoDB full-fledged app Part 2: User profile and Profile Picture
Tags: react, javascript, mongodb, tutorial
Published: 2020-03-29 03:39:06

This is a follow-up to [Part 1](https://hoangvvo.com/blog/next-js-and-mongodb-app-2 "Next.js and MongoDB full-fledged app Part 1: User authentication"). Make sure you read it before this post. Today, I will showcase how I add editable user profile and profile Picture.

Again, below are the Github repository and a demo for this project to follow along.

[Github repo](https://github.com/hoangvvo/nextjs-mongodb-app "nextjs mongodb app")

[Demo](https://nextjs-mongodb.now.sh/)

## About `nextjs-mongodb-app` project

> nextjs-mongodb-app is a Full-fledged serverless app made with Next.JS and MongoDB

Different from many other Next.js tutorials, this:

- Does not use the enormously big Express.js
- Supports `serverless`
- Using Next.js v9 API Routes w/ middleware

For more information, visit the [Github repo](https://github.com/hoangvvo/nextjs-mongodb-app "nextjs mongodb app").

## What we are making

![Edit Profile Page](https://hoangvvo.com/blog/next-js-and-mongodb-app-2/edit-profile-page.gif)

(The GIF above is actually from an older version. Looks super bare-bone 😜)

We are adding the following features:

- Profile Page
- Edit Profile
- Profile Picture

## The user profile page

![Profile Page](https://hoangvvo.com/blog/next-js-and-mongodb-app-2/profile-page.png)

My user profile page will be at `/user/my-username`. Let's create `/pages/user/[username]/index.jsx` so we can [dynamically](https://nextjs.org/docs/routing/dynamic-routes) show user profile based on the `username` param.

```jsx
import { database } from "@/api-lib/middlewares";
import nc from "next-connect";
import Head from "next/head";

const ProfilePage = ({ user }) => {
  return (
    <>
      <Head>
        <title>
          {user.name} (@{user.username})
        </title>
      </Head>
      <div>
        <img
          src={user.profilePicture}
          width="128"
          height="128"
          alt={user.name}
        />
        <h1>
          <div>{user.name}</div>
          <div>@{user.username}</div>
        </h1>
      </div>
    </>
  );
};

export async function getServerSideProps(context) {
  await nc().use(database).run(context.req, context.res);

  const user = await req.db
    .collection("users")
    .findOne(
      { username: context.params.username },
      { projection: { password: 0, email: 0, emailVerified: 0 } }
    );

  if (!user) {
    return {
      notFound: true,
    };
  }
  user._id = String(user._id); // since ._id of type ObjectId which Next.js cannot serialize
  return { props: { user } };
}

export default ProfilePage;
```

For the above, we use [getServerSideProps](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering) to retrieve the user data from the database.
Our [`database` middleware](https://hoangvvo.com/blog/next-js-and-mongodb-app-1#database-middleware) is used to load the database into `req.db`. This works because `getServerSideProps` is run on the server-side.

Then, we call MongoDB `findOne()` to retrieve the user by the username from params (`context.params.username`). You can also notice that we filter out the sensitive fields via `projection`.

If the user is found, we return it as a prop. Otherwise, we return the not found page by [setting notFound to `true`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering).

Our page component would receive the `user` prop as to render his or her information.

## The Profile Setting page

### Building the Profile Update API

The way for our app to update the user profile is would be to make a **PATCH** request to `/api/user`.

In `pages/api/user/index.js`, we add a handler for **PATCH**:

```javascript
import { auths, database, validateBody } from "@/api-lib/middlewares";
import nc from "next-connect";
import slug from "slug";

const handler = nc();

handler.use(database, ...auths);

handler.patch(
  validateBody({
    type: "object",
    properties: {
      username: { type: "string", minLength: 4, maxLength: 20 },
      name: { type: "string", minLength: 1, maxLength: 50 },
      bio: { type: "string", minLength: 0, maxLength: 160 },
    },
  }),
  async (req, res) => {
    if (!req.user) {
      req.status(401).end();
      return;
    }
    const { name, bio } = req.body;

    if (req.body.username) {
      username = slug(req.body.username);
      if (
        username !== req.user.username &&
        (await req.db.collection("users").countDocuments({ username })) > 0
      ) {
        res
          .status(403)
          .json({ error: { message: "The username has already been taken." } });
        return;
      }
    }

    const user = await db
      .collection("users")
      .findOneAndUpdate(
        { _id: new ObjectId(id) },
        {
          $set: {
            ...(username && { username }),
            ...(name && { name }),
            ...(typeof bio === "string" && { bio }),
          },
        },
        { returnDocument: "after", projection: { password: 0 } }
      )
      .then(({ value }) => value);

    res.json({ user });
  }
);
```

We first validate the body using our [validateBody middleware](/blog/next-js-and-mongodb-app-1#request-body-validation-middleware). Then, we check if the user is logged in by checking `req.user`. If not, it will send a [401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) response.

If a username is provided, we will slugify it and check if exists in the database. Finally, we call MongoDB [findOneAndUpdate](https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/) to update the user profile based on the data from `req.body`.

We then return the updated user document.

### The Profile Settings Page

![Profile Settings Page](https://hoangvvo.com/blog/next-js-and-mongodb-app-2/profile-settings-page.png)

The next thing to do is have page at `/settings` for us to update our info.

Let's create `pages/settings.jsx`

```jsx
import { useCurrentUser } from "@/lib/user";
import { fetcher } from '@/lib/fetch';
import { useRouter } from "next/router";
import { useEffect, useCallback } from "react";

const AboutYou = ({ user, mutate }) => {
  const usernameRef = useRef();
  const nameRef = useRef();
  const bioRef = useRef();

  const onSubmit = useCallback(
    async (e) => {
      e.preventDefault();
      try {
        const formData = new FormData();
        formData.append("username", usernameRef.current.value);
        formData.append("name", nameRef.current.value);
        formData.append("bio", bioRef.current.value);
        const response = await fetcher("/api/user", {
          method: "PATCH",
          body: formData,
        });
        mutate({ user: response.user }, false);
      } catch (e) {
        console.error(e.message);
      }
    },
    [mutate]
  );

  useEffect(() => {
    usernameRef.current.value = user.username;
    nameRef.current.value = user.name;
    bioRef.current.value = user.bio;
  }, [user]);

  return (
    <form onSubmit={onSubmit}>
      <input ref={usernameRef} placeholder="Your username" />
      <input ref={nameRef} placeholder="Your name" />
      <textarea ref={bioRef} placeholder="Your bio" />
      <button type="submit">Save</button>
    </form>
  );
};

const SettingsPage = () => {
  const { data, error, mutate } = useCurrentUser();
  const router = useRouter();
  useEffect(() => {
    if (!data && !error) return; // useCurrentUser might still be loading
    if (!data.user) {
      router.replace("/login");
    }
  }, [router, data, error]);
  if (!data?.user) return null;
  return <AboutYou user={data.user} mutate={mutate} />;
};

export default SettingsPage;
```

First of all, the settings page should only be available to authenticated users only. Therefore, if the current user is not available, we want to navigate to `/login`, which I do so using `router` and our `useCurrentUser` hook.

For the update form, we simply create an `onSubmit` function that collects the inputs and makes a _PATCH_ request to our just created API at `/api/user`.

Every time the `user` prop is updated, we need to set the values of the inputs accordingly, which I do so inside the above `useEffect`.

One thing to note is that we use [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) to send our fields instead of the regular `application/json`. The reason for this is that it allows us to include our profile picture later, which can be conveniently transmitted via FormData, in the same request.

Upon receiving a successful response, we call `mutate` to update the SWR cache.

## Building the Profile picture functionality

To have this functionality, we need somewhere to host our images. I choose [Cloudinary](https://cloudinary.com) to host my images, but you can use any service.

### Add profile picture to the settings page

In the same form above, we add our profile picture field:

```jsx
<input type="file" accept="image/png, image/jpeg" ref={profilePictureRef} />
```

(note: the screenshot actually above puts this input in front of an image to achieve the effect as seen, see the [source code](https://github.com/hoangvvo/nextjs-mongodb-app/blob/46caae0349c79bd75522b43726bf278182925f06/page-components/Settings/index.jsx#L174-L183))

This field has a ref of `profilePictureRef`, allowing us to access its value:

```javascript
const profilePictureRef = useRef();
```

Adding into our existing `onSubmit` function:

```javascript
/* ... */
if (profilePictureRef.current.files[0]) {
  formData.append("profilePicture", profilePictureRef.current.files[0]);
}
```

If the user did select an image, we can access its value in `profilePictureRef.current.files[0]` (`files` is an array because it can be a multi-file upload) and add it to our `FormData` instance.

It will be included in the same **PATCH** request.

### Building the Profile Picture Upload API

Since our profile picture is submitted to the same `PATCH` endpoint. Let's edit its handler.

To handle images, we need something to parse the uploaded file. [Multer](https://www.npmjs.com/package/multer) is the package that we will use.

Let's take a look at our **PATCH** handler again:

```javascript
import { auths, database, validateBody } from "@/api-lib/middlewares";
import nc from "next-connect";
import slug from "slug";

import multer from "multer";
const upload = multer({ dest: "/tmp" });

const handler = nc();

handler.use(database, ...auths);

handler.patch(
  upload.single("profilePicture"),
  validateBody({
    type: "object",
    properties: {
      username: { type: "string", minLength: 4, maxLength: 20 },
      name: { type: "string", minLength: 1, maxLength: 50 },
      bio: { type: "string", minLength: 0, maxLength: 160 },
    },
  }),
  async (req, res) => {
    /* ... */
  }
);

export const config = {
  api: {
    bodyParser: false,
  },
};

export default handler;
```

Looking at:

```javascript
export const config = {
  api: {
    bodyParser: false,
  },
};
```

I am disabling Next.js 9 body-parser because form parsing is already handled by `Multer`.

We initialize an instance of `Multer` that is configurated to save the file to our temp folder:

```javascript
const upload = multer({ dest: "/tmp" });
```

The instance itself is a middleware, so we attach it before our main handler in the **PATCH** handlers. The middleware expects a single file upload under the `profilePicture` field that we specified earlier in our form submission function. Now, we can access the file via `req.file`.

```javascript
handler.patch(
  upload.single("profilePicture"),
  validateBody({
    /* ... */
  }),
  async (req, res) => {
    console.log(req.file);
  }
);
```

#### Integrate Cloudinary

This is the section for the file uploading logic. The content in this section depends on the File Uploading library or service you choose. I am using Cloudinary in my case.

If you use Cloudinary, go ahead and create an account there.

Cloudinary provides its [Javascript SDK](https://www.npmjs.com/package/cloudinary "Cloudinary").

To configure `Cloudinary`, we need to set the following environment variable:

`CLOUDINARY_URL=cloudinary://my_key:my_secret@my_cloud_name`

The `Environment variable` value can be found in the **Account Details** section in [Dashboard](<https://cloudinary.com/console> "Cloudinary Dashboard). (Clicking on `Reveal` to display it)

If you use Cloudinary, look at its [Node.js SDK documentation](https://cloudinary.com/documentation/node_integration) for more information.

Import the cloudinary SDK (Using its `v2`):

```javascript
import { v2 as cloudinary } from "cloudinary";
```

Uploading an image is as simple as:

```javascript
cloudinary.uploader.upload("theImagePath");
```

...where out image path is `req.file.path`.

```javascript
let profilePicture;
if (req.file) {
  const image = await cloudinary.uploader.upload(req.file.path, {
    width: 512,
    height: 512,
    crop: "fill",
  });
  profilePicture = image.secure_url;
}

const user = await updateUserById(req.db, req.user._id, {
  ...(username && { username }),
  ...(name && { name }),
  ...(typeof bio === "string" && { bio }),
  ...(profilePicture && { profilePicture }), // <- set the url to our user document
});
```

We are uploading our image to `Cloudinary` with the option of cropping it down to `512x512`. You can set it to whatever you want or not have it at all. If the upload is a success, I set the URL (the secured one) of the uploaded image to our user's `profilePicture` field. See [cloudinary#upload](https://www.npmjs.com/package/cloudinary#upload) for more information.

Awesome, we have managed to create our Profile Picture functionality.

## Conclusion

Let's run our app and test it out. We have managed to create our user profile functionality with profile picture.

Again, check out the repository [nextjs mongodb app](https://github.com/hoangvvo/nextjs-mongodb-app).

If you find this helpful, consider give the the repo a star to motivate me to add more content.

Good luck on your next Next.js + MongoDB project!