DEV Community

loading...

Getting TypeORM to work with Next.js and TypeScript

unframework profile image Nick Matantsev Updated on ・5 min read

I was setting up a new Next.js app (using NextAuth for SSO) and decided to use TypeORM as the database persistence layer. Next.js is great at running TypeScript code without hiccups and TypeORM, while new to me, seemed promising in its minimalism. However, I ran into some non-trivial error messages and bugs trying to get them to play together. And even though most answers were already discovered by other kind folks on the internet, it took me a while to track it all down and combine together into one solution.

There were three key areas to resolve:

  • importing TypeScript-formatted entities in Next.js and CLI contexts
  • support for TypeORM's decorator syntax + reflect-metadata in Next.js build pipeline
  • preventing hot-module-reload (HMR) class instance confusion during development runtime

Importing Entities and Connection Config

Normally, TypeORM runs inside a vanilla Node.js environment, which means it cannot consume TypeScript files (such as entity class definitions) without precompilation. For example, there is a common error that happens when the entities path in TypeORM config refers to TS source files (i.e. src/entity/*.ts):

Error during schema synchronization:
/project/path/src/entity/User.ts:1
import {
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:979:16)
...
Enter fullscreen mode Exit fullscreen mode

With Next.js and its built-in support for TypeScript I did not expect this problem to happen, but still ended up encountering the above error message.

The reason is that when TypeORM creates a new connection, it tries to load all the entity class files dynamically based on a path wildcard. That process bypasses the entire Next.js Babel bundling pipeline and falls back on Node.js's built-in module loader. So even though my Next.js server code can import and run entity class TS files just fine, the TypeORM connection initializer lives in a "parallel universe" and naively tries to load them from scratch on its own, which then fails.

I tried to use ts-node to compile TS modules on the fly as they get loaded by TypeORM, but then I got a different kind of error:

RepositoryNotFoundError: No repository for "User" was found. Looks like this entity is not registered in current "default" connection?
Enter fullscreen mode Exit fullscreen mode

In this scenario I ended up having two clones of each entity class co-existing in memory: one loaded and instantiated by TypeORM + ts-node, and another one bundled by Next.js pipeline with the rest of server code. Hence the class reference confusion.

Instead, my approach was to name and import all the entity files explicitly, without an entities wildcard, like so:

import { User } from './entity/User';
import { Account } from './entity/Account';
// etc
Enter fullscreen mode Exit fullscreen mode

And then pass an explicit options object to createConnection() with entity classes directly referenced like this:

createConnection({
  entities: [
    User,
    Account,
    // etc
  ]
})
Enter fullscreen mode Exit fullscreen mode

Correspondingly, I removed the ormconfig.js file to avoid any further conflicts.

I did keep around a separate ormconfig.cli.js file just for CLI schema sync and migrations. For that to work, I installed ts-node and added require('ts-node/register') to the top of the config file so that TS entity definitions can be loaded with no extra fuss. The command-line script looks like this:

typeorm --config ormconfig.cli.js schema:sync
Enter fullscreen mode Exit fullscreen mode

Decorator Syntax and reflect-metadata in Next.js

TypeORM entity class definitions use decorator syntax (e.g. @Entity(), @Column(), etc). Also, there has to be a bit of special plumbing to let TypeORM read TypeScript field types such as string and infer database column types such as varchar. To make the above work, the TypeORM documentation asks to install a package called reflect-metadata and also to tweak tsconfig.json to set emitDecoratorMetadata and experimentalDecorators to true.

However, Next.js does not use TSC (the original TypeScript compiler) and instead relies on Babel's @babel/preset-typescript package. Because of that, those tsconfig.json tweaks do not have any effect.

Instead, I added custom Babel configuration in my Next.js project and included the equivalent options for Babel (see this issue about decorator support and this issue about metadata). This is what's in the resulting .babelrc file:

{
  "presets": [
    [
      "next/babel",
      {
        "class-properties": {
          "loose": true
        }
      }
    ]
  ],
  "plugins": [
    "babel-plugin-transform-typescript-metadata",
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}
Enter fullscreen mode Exit fullscreen mode

Extra packages needed to be installed (class-properties plugin is already included with Next.js): @babel/plugin-proposal-decorators babel-plugin-transform-typescript-metadata @babel/core.

Note: you can omit installing reflect-metadata and babel-plugin-transform-typescript-metadata if you specify database column types explicitly. Then TypeORM does not have to infer anything from TS types. For some folks that might be preferable from a stability perspective, but at the cost of being more verbose.

Next.js HMR and TypeORM Entity Classes

Hot-module-reloading (HMR) throws another monkey-wrench in the works.

During development, every time you edit your Next.js pages, API routes or other files like entity classes your code ends up being recompiled and reloaded from scratch. Because TypeORM connection manager is not aware of entity class reloads, the connection object quickly gets out of sync and stops being useful.

E.g. if you have a User entity class, Next.js will load and create a class reference for it - let's call it "User v1". That reference is passed to createConnection and of course the rest of your code uses it too. Now, once you edit that class file, Next.js will perform a hot reload, and there are now two different class references living inside runtime memory. One is the original "User v1" and another one is the freshly recompiled "User v2". Your route code is now using the "User v2" class reference, but the connection still has "User v1" in its list of known entities. When you try to e.g. call getRepository(User) in your code, you will not be passing the same class reference as what TypeORM "knows", so you will get this error again:

RepositoryNotFoundError: No repository for "User" was found. Looks like this entity is not registered in current "default" connection?
Enter fullscreen mode Exit fullscreen mode

I saw a few GitHub issues discussing solutions to this (such as this workaround). For me, the ultimate answer was to simply get any prior connection and close it before opening a new one.

Here is a sample code snippet; I put it in a shared central file like src/db.ts:

let connectionReadyPromise: Promise<void> | null = null;

function prepareConnection() {
  if (!connectionReadyPromise) {
    connectionReadyPromise = (async () => {
      // clean up old connection that references outdated hot-reload classes
      try {
        const staleConnection = getConnection();
        await staleConnection.close();
      } catch (error) {
        // no stale connection to clean up
      }

      // wait for new default connection
      await createConnection({
        // connection options go here
      });
    })();
  }

  return connectionReadyPromise;
}
Enter fullscreen mode Exit fullscreen mode

Then in any function that calls out to TypeORM I add this before performing any database actions:

await prepareConnection();
Enter fullscreen mode Exit fullscreen mode

It may seem a bit onerous to include this in many different spots but there always has to be some sort of wait-until-ready logic for database usage anyway, so this ends up serving that exact purpose.

Conclusion

I hope that the TypeORM docs eventually include Babel-specific config recipes and HMR-friendly "connection refresh" helpers like the above. Also, the dynamic wildcard loader for entities would ideally be pluggable into a bundler pipeline like Next.js's. But for now this combination of settings worked pretty well, and I hope it helps you too!

Discussion (0)

pic
Editor guide