Load data from a file in Remix and Vercel

TL;DR

For some weird reason, you cannot read files from Remix the same way you read files from Next.js on Vercel.

The workaround is to use fs.readFile(__dirname + '/../json/data.json', 'utf8'). Instead if using process.cwd() and path.join().

This workaround was found the hard way by a couple of people mentioned below, however, it's not documented properly and escaped me for months.

Here goes my attempt to spread this workaround until it's officially documented or fixed by Vercel.

Why is this happening?

When I tweeted about this workaround, Vercel's CEO Guillermo Rauch explained the reason beind this behavior:

The reason is that file references must be statically analyzable.

The asset keyword in JS would come very handy here. Otherwise we run the risk of overfitting a potentially gigantic filesystem in the critical path of your cold function invocation.

More weight = slower site

— Guillermo Rauch (@rauchg)

July 17, 2022

Guillermo also confirmed that Next.js is doing some magic to make process.cwd() and path.join() work.

Below is a detailed guide on how to read files in Remix and Vercel

Set up the json data

Create a basic Remix.run rpoject with npx create-remix@latest and create a json folder in the root of the project, and inside it, create a data.json file.

┌──	api
├──	app
├── json
│	└──	data.json
├── # ...

Then paste the following code in the data.json file:

{
  "record": {
    "id": 8221,
    "uid": "a15c1f1d-9e4e-4dc7-9c45-c04412fc5064",
    "name": "Remix.run",
    "language": "TypeScript"
  }
}

Read the file in the route's loader

In app/routes/index.tsx, add the following code:

import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

import { promises as fs } from "fs";

export const loader = async (args: LoaderArgs) => {
  // Find the absolute path of the json directory
  // Note: As of July 17, 2022, Vercel doesn't include the json directory when using process.cwd() or path.join(). The workaround is to use __dirname and concatenate the json directory to it.
  const jsonDirectory = __dirname + "/../json";
  // Read the json data file data.json
  const fileContents = await fs.readFile(jsonDirectory + "/data.json", "utf8");
  // Parse the json data file contents into a json object
  const data = JSON.parse(fileContents);

  return json({
    data,
  });
};

Run your application locally using npm run dev and browse to http://localhost:3000 and you should see:

{"data":{"record":{"id":8221,"uid":"a15c1f1d-9e4e-4dc7-9c45-c04412fc5064","name":"Remix.run","language":"TypeScript"}}}

Display the data in the route's component

To display the returned data in the route's component, add the following code to the same file:

// ...
import { useLoaderData } from '@remix-run/react'

// ...
export default function Index() {
	const { data } = useLoaderData<typeof loader>()

	return (
		<div>
			<h1>My Framework from file</h1>
			<ul>
				<li>Name: {data.record.name}</li>
				<li>Language: {data.record.language}</li>
			</ul>
		</div>
	)
}

You should see the following error in the browser:

Error: Cannot initialize 'routeModules'. This normally occurs when you have
server code in your client modules. Check this link for more details:
https://remix.run/pages/gotchas#server-code-in-client-bundles

This is because the node:fs/promises module we imported in the route made it into browser bundles as explained in Remix's docs.

To make sure it's used on the server only, create a utils directory inside app, and inside it, create fs-promises.server.ts.

├──	# ...
├──	app
│	└──	routes
│	└──	utils
│		└──	fs-promises.server.ts
├──	# ...

Then export the node:fs/promises module from the fs-promises.server.ts file:

export { promises as fs } from "fs";

Now you can import node:fs/promises from the fs-promises.server.ts instead as follows:

// ...
import { fs } from "~/utils/fs-promises.server";
// ...

When you brose to http://localhost:3000 you should see the same data as on this demo.

displayed data from the json file

That's all!

Here's the source code of the demo that's deployed to Vercel.