← Back to Blog

How to Build a Native Mobile App with Next.js and Capacitor

If you already have a Next.js app, it's possible to build it into a native mobile app that can be made available in the Play Store or the App Store. If you're familiar with Next.js and React already then you may want to develop your app using those technologies rather than having to use something you're unfamiliar with.

person holding phone with app open

If you're looking for someone to do this for you or you need help, reach out to Enesien Software out of Nashville to get an estimate. We're experts in cross-platform app development, particularly Next.js apps.

In this guide we will go over the steps required to build your Next.js app as a native mobile app.

Before we begin, there are a few caveats to consider before using Next.js.

  • You will not be able to use SSR technology like getServerSideProps - like not at all...seriously.
  • You will not be able to use image optimization, although you can still use the <Image /> component
  • If you still want to use your /api endpoints, you will need to allow cors on your endpoints and modify your outgoing fetch() requests to include the full URL
  • If you use tailwind, you'll need to replace any usage of gap- classes with space-x- classes, as many modern mobile browsers still don't support then underlying css

Why use Next.js and not something else, like vanilla React or Angular?

This mostly depends on the individual or company who's developing the app. For us at Enesien, we wanted a web application as well as a mobile app for some of our clients. Since we love Next.js and the ecosystem around it, it sometimes makes sense for us to build an app once with Next.js and have it made available as a native app and a web app using the same codebase.

next.js code on computer screen

Let's Do It

Essentially you'll be following the installation guide from Capacitor to start, but this is a rundown of what we do.

First, open your project and install the required Capacitor libraries.

npm i @capacitor/core
npm i -D @capacitor/cli

Now, initialize Capacitor:

npx cap init

Add the platforms you want to use. We'll just start with Android

npm i @capacitor/android
npx cap add android

Now, we'll change the output directory for Capacitor to look at when it copies app assets to the respective platforms during sync.

Open capacitor.config.ts and change the value of the webDir property to "out":

webDir: 'out'

Next we'll need to configure Next.js to build properly for use in a Capacitor app.

If you are using the same codebase for your Next.js web app and your mobile app, you'll need to create 2 different next configuration files, one that's used when building for mobile and one that's used for building on web, like with Vercel.

create next.config.default.js and next.config.export.js

The default config will be your web config and the export config will be used for mobile.

Your next.config.default.js will probably look something like this:

/** @type  {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};
module.exports = nextConfig;

For mobile, we have to turn on the export build and turn off image optimization, so you want your next.config.export.js to look something like this:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  output: "export", // this is what builds the assets we'll use in the mobile app
  images: {
    unoptimized: true, // this is required for building for mobile
  },
};
module.exports = nextConfig;

Now we'll write some scripts to copy the proper config file before running builds. In your package.json file, add the following scripts:

"dev": "copy next.config.default.js next.config.js && next dev",
"export": "copy next.config.export.js next.config.js && next build && copy next.config.default.js next.config.js && npx cap sync"

The modified dev script simply copies the config for web prior to serving your app locally.

The export script copies the config for mobile prior to running the build. Then it builds as an export build, exporting the compiled project to the out directory. Then it copies back the default config and runs a Capacitor sync operation. This copies assets from out to the respective native build directories and syncs up all other Capacitor plugins, etc.

Now we can open the project in Android Studio. If you haven't already, you will need to setup Android Studio for the first time. More information from Capacitor on how to do that here.

npx cap open android

Anytime you make changes to the Next.js app now, you'll do npm run export and then the changes will be in Android Studio. If you modify your API endpoints, you will need to deploy your app to live servers, first.

Using Live APIs from Within the App

You will likely still want to use your API endpoints from the /pages/api/ directory from your mobile app. These endpoints are obviously not part of the native app build itself, but you can build for the web and deploy the endpoints which your app can then use.

However, you will need to provide your fetch operations with a complete URL instead of the relative URLs you're likely using. For example,

fetch('/api/hello') will need to be changed to fetch('https://your-app.vercel.app/api/hello')

We typically do this using a prefix variable, like so:

const prefix = isProd ? "https://your-app.vercel.app" : "";
fetch(`${prefix}/api/hello`);

Because HTTP requests from the mobile app will be coming from http://localhost (this is how Capacitor is configured by default) you'll have to allow CORS from all of your API endpoints, or requests may get blocked on their way out. There are a few ways to accomplish this, but I opted for a middleware function that I pass API handler functions through.

cors.ts

/**
 * This is the "middleware" that will be required for endpoints that are called from the mobile app.
 */
export const cors: (next: NextApiHandler) => NextApiHandler =
  (next) => async (req, res) => {
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods", "OPTIONS,GET,POST");
    res.setHeader(
      "Access-Control-Allow-Headers",
      "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Authorization, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
    );
    if (req.method === "OPTIONS") {
      res.status(200).end();
      return;
    }
    return next(req, res);
  };

/api/hello.ts

async function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ success: true });
}
export default cors(handler);

thumbs up done

That's it - godspeed!

Gavin Johnsen

Owner, Enesien Software


Partner with
Enesien today.

Please share details about your project, and we'll schedule a complimentary consultation call to explore how we can be of assistance.
Gavin Johnsen, Owner of Enesien Software

Gavin D Johnsen
Owner

(615) 905-6801Phone number
- or -
(615) 905-6801Phone number