DEV Community

Nick Matantsev
Nick Matantsev

Posted on • Edited on • Originally published at unframework.com

Getting TypeORM to work with Next.js and TypeScript

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!

Top comments (10)

Collapse
 
drkgrntt profile image
Derek Garnett

Did you experience this error?
ReferenceError: Cannot access 'User' before initialization

I'm having trouble getting past this. I think it's due to the fact that I'm using relations between entities and it's creating a circular dependency with how NextJS compiles it. I've seen it referenced in a few places online, but I haven't found any solutions that have worked for me.

Collapse
 
muhaimincs profile image
Muhaimin CS

still having the same error without no solution

Collapse
 
drkgrntt profile image
Derek Garnett

If this is the error I remember, I ended up making all my relation types partials.

Thread Thread
 
ryancraigmartin profile image
Ryan Craig Martin

Hey Derek, mind posting an example of how you fixed this?

Thread Thread
 
drkgrntt profile image
Derek Garnett

In short: comments!: Partial<Comment[]>. I made every relationship type to another model a Partial of it.

You can see all of my models at github.com/papyrcms/papyrcms/blob/.... The example is from the Blog model, but several are like this.

It's not elegant and I'm strongly considering moving away from TypeORM for this project. It worked beautifully in a different project with a persistent server, but not so elegantly in this one using static API routes that Next provides.

Collapse
 
makanamakesstuff profile image
Makana Edwards

I'm late, but I resolved this by using interfaces for typing instead of the Entities themselves.

User:

export interface UserData {
    id: number
    username: string
    email: string
    password: string
    meta?: UserMetaData[]
    SetPassword(): Promise<void>
}

@Entity()
export class User implements UserData {
    @PrimaryGeneratedColumn()
    declare id: number

    @Column({ unique: true })
    declare username: string

    @Column({ unique: true })
    declare email: string

    @Column()
    declare password: string

    @OneToMany(() => UserMeta, (meta) => meta.user, {
        cascade: true,
    })
    declare meta?: UserMetaData[]

    @BeforeInsert()
    async SetPassword() {
        try {
            const salt = await bcyrpt.genSalt(8)
            const hashed = await bcyrpt.hash(this.password, salt)
            this.password = hashed
        } catch (error) {
            console.error(`Failed to encrpt password for ${this.username}`)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

User Meta:

export interface UserMetaData {
    uuid?: string
    name: string
    value: string
    user?: UserData
}

@Entity()
export class UserMeta {
    @PrimaryGeneratedColumn()
    declare uuid?: string

    @Column()
    declare name: string

    @Column()
    declare value: string

    @ManyToOne(() => User, (user) => user.meta, {
        onDelete: "CASCADE",
        onUpdate: "CASCADE",
        orphanedRowAction: "delete",
    })
    @JoinColumn({ name: "user_id" })
    declare user?: UserData
}

Enter fullscreen mode Exit fullscreen mode

I hope this helps future folk!

Collapse
 
noitidart profile image
Noitidart

I'm very new to both next.js and typeorm, do you have a more beginners guide friendly article? Maybe a github repo I can reference? Maybe a step by step how to create first entity in typeorm after installing into next.js?

Collapse
 
aaronngray profile image
Aaron Gray

A working example repo would save a lot of time !

Collapse
 
ilx profile image
Mr. R | #RestoreTheSnyderVerse

All I can say is I f**king love you! I've been struggling with this problem for hours/days and thanks to you I got things working now.

Collapse
 
r11 profile image
Peter Jaffray

Thank you for this. Trying it now.