Creating Admin Panels for NestJs and Prisma Made Easy with AdminJS
Published: 2023-04-28 12:28:33

---
title: Creating Admin Panels for NestJs and Prisma Made Easy with AdminJS
published: true
date: 2023-03-21 12:52:51 UTC
tags: 
canonical_url: https://thecodeway.hashnode.dev/creating-admin-panels-for-nestjs-and-prisma-made-easy-with-adminjs
---

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679402031888/9d03333a-b47c-49c0-8fe0-ae8ea13424b4.png?w=1600&h=840&fit=crop&crop=entropy&auto=compress,format&format=webp)

Are you tired of spending too much time creating admin panels for your NestJs and Prisma applications? Look no further than AdminJS! This powerful framework allows you to create a fully functional admin dashboard with just a few lines of code. With its compatibility with various ORM and ODM models, it simplifies the process of performing CRUD operations on your models.

Plus, with customization options available, you can tailor the dashboard to meet your specific project needs. Keep reading to learn how to get started with AdminJS and take control of your application's data management.

### What is ORM

Object-relational mapping (ORM, O/RM, and O/R mapping tool) in computer science is a programming technique for converting data between incompatible type systems using object-oriented programming languages.

### What is ODM?

Object Document Mapper (ODM) is a library that helps you to work with MongoDB databases. It provides an abstraction layer between the database and the application code. It allows you to use the object-oriented paradigm to work with the database.

## Getting Started

**AdminJs/NestJs => AdminJs/Prisma => Resources and Customizations**

Above is a very simple flow that describes the process of setting up AdminJs on your NestJs project.

**Install Requirements**

```
yarn add adminjs @adminjs/nestjs @adminjs/express express-session express-formidable

```

**app.module.ts**

```
import { Module } from '@nestjs/common'
import { AdminModule } from '@adminjs/nestjs'

import { AppController } from './app.controller'
import { AppService } from './app.service'

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

@Module({
  imports: [
    AdminModule.createAdminAsync({
      useFactory: () => ({
        adminJsOptions: {
          rootPath: '/admin',
          resources: [],
        },
        auth: {
          authenticate,
          cookieName: 'adminjs',
          cookiePassword: 'secret'
        },
        sessionOptions: {
          resave: true,
          saveUninitialized: true,
          secret: 'secret'
        },
      }),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

```

### Don't want authentication?

You might want your admin panel to not require authentication depending on the project you are working on. By removing the `auth` and `sessionOptions` from the object returned by the [factory provider](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory).

```
...
AdminModule.createAdminAsync({
  useFactory: () => ({
    adminJsOptions: {
      rootPath: '/admin',
      resources: [],
    },
  }),
}),
...

```

**Start your server**

```
nest start --watch
# OR
nest start

```

Visit `127.0.0.1:<your port>/admin` . If you have authentication enabled it will automatically redirect you to the login page.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679400981499/d788bc7f-0dd7-4bbd-ae25-38f593fcf18f.png)

Enter the details from the `DEFAULT_ADMIN` object in the `app.module.ts` program.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679401202328/655870a7-27bf-42da-9e08-595796e20766.png)

### Adding Database models

Upon logging in, if you're unable to locate your models on the dashboard, resources can be of great help. AdminJs leverages the resources added to its options to effectively showcase, evaluate, and utilize them.

Assuming you have this Prisma schema,

```
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url = env("DATABASE_URL")
}

model User {
  pk Int @id @default(autoincrement())
  id String @unique @default(uuid()) @db.Uuid
  email String @unique
  password String
  active Boolean @default(true)
  staff Boolean @default(false)
  admin Boolean @default(false)
  profile Profile?

  @@unique([pk, id])
  @@map("users")
}

model Profile {
  pk Int @id @default(autoincrement())
  id String @unique @default(uuid()) @db.Uuid
  firstName String? @map("first_name")
  lastName String? @map("last_name")
  userPk Int @unique @map("user_pk")
  userId String @unique @map("user_id") @db.Uuid
  user User @relation(fields: [userPk, userId], references: [pk, id], onDelete: Cascade, onUpdate: Cascade)

  @@unique([pk, id])
  @@unique([userPk, userId])
  @@map("profiles")
}

```

**app.module.ts**

```
import { Module } from '@nestjs/common'
import { AdminModule } from '@adminjs/nestjs'
import { Database, Resource } from '@adminjs/prisma';
import AdminJS from 'adminjs'
import { DMMFClass } from '@prisma/client/runtime';
import { PrismaService } from '@database/prisma.service';
import { DatabaseModule } from '@database/database.module';

import { AppController } from './app.controller'
import { AppService } from './app.service'

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

AdminJS.registerAdapter({ Database, Resource });

@Module({
  imports: [
    AdminModule.createAdminAsync({
      imports: [DatabaseModule],
      useFactory: (prisma: PrismaService) => {
        const dmmf = (prisma as any)._baseDmmf as DMMFClass;

        return {
          adminJsOptions: {
            rootPath: '/admin',
            resources: [
              {
                resource: { model: dmmf.modelMap['User'], client: prisma },
                options: {},
              },
              {
                resource: { model: dmmf.modelMap['Profile'], client: prisma },
                options: {},
              },
            ],
          },
          auth: {
            authenticate,
            cookieName: 'adminjs',
            cookiePassword: 'secret'
          },
          sessionOptions: {
            resave: true,
            saveUninitialized: true,
            secret: 'secret'
          },
        }
      },
      inject: [PrismaService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

```

Explore the basic Resource object format and effortlessly add multiple resources to your project.

```
{
  resource: { model: dmmf.modelMap[{Model name here}], client: prisma },
  options: {},
}

```

Make sure you replace `{Model name here}` with the name of the model you want to use. For example, if you have a model `Post`, you would replace `{Model name here}` with `Post`.

#### How does this work?

The Database Model Mapper (DMMF) object comprises a comprehensive depiction of your database schema. Its functionality extends to facilitating the extraction of your model from Prisma. The DMMF achieves this by mapping model names to their respective model objects. For example, to retrieve the User model from the map, one can utilize the syntax `dmmf.modelMap['User']`. Once the model object is obtained, it can be assigned to the resource's model property.

The Prisma client serves as the client property, facilitating connectivity to the database for conducting CRUD operations.

You can add as many resources as you need. For example, if you have a `Post` model in your Prisma schema that you want to be displayed in the Admin Dashboard. You just need to add it to the resources array;

**app.module.ts**

```
...
{
  resource: { model: dmmf.modelMap['Post'], client: prisma },
  options: {},
},
...

```

Now restart your server if it's not on fast reload.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679401266016/7121f311-b9c8-49fb-8d8e-38d2572414fe.png)

### Simplify adding resources

When managing a substantial number of database models, it is common to encounter the need for duplicating codes while incorporating new resources for each model.

Did you know that the [Builder pattern](https://en.wikipedia.org/wiki/Builder_pattern#:~:text=The%20builder%20pattern%20is%20a,Gang%20of%20Four%20design%20patterns.) can help you add multiple resources to your code without repeating the same code? This makes your code more efficient and easier to maintain. So why not give it a try?

```
import { Module } from '@nestjs/common'
import { AdminModule } from '@adminjs/nestjs'
import { Database, Resource } from '@adminjs/prisma';
import AdminJS from 'adminjs'
import { DMMFClass } from '@prisma/client/runtime';
import { PrismaService } from '@database/prisma.service';
import { DatabaseModule } from '@database/database.module';
import { ResourceWithOptions, ResourceOptions } from 'adminjs/types/src';

import { AppController } from './app.controller'
import { AppService } from './app.service'

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

AdminJS.registerAdapter({ Database, Resource });

class CResource {
  model: any;
  options: ResourceOptions;

  constructor(model: any, options?: ResourceOptions) {
    this.model = model;
    this.options = options || {};
  }
}

class CResourceBuilder {
  private readonly resources: Array<CResource> = [];
  dmmf: DMMFClass;

  constructor(private readonly prisma: PrismaService) {
    this.dmmf = ((prisma as any)._baseDmmf as DMMFClass)
  }

  /**
   * Adds a resource to the builder
   * 
   * @param resource string
   * @param options ResourceOptions
   * @returns this
   */
  public addResource(resource: string, options?: ResourceOptions): this {
    const obj = new CResource(this.dmmf.modelMap[resource], options);
    this.resources.push(obj);
    return this;
  }

  /**
   * Compiles the resources into an array of objects
   * that can be passed to the AdminJS module
   * 
   * @returns Array<ResourceWithOptions | any>
   */
  public build(): Array<ResourceWithOptions | any> {
    return this.resources.map((resource) => {
      return {
        resource: {
          model: resource.model,
          client: this.prisma,
        },
        options: resource.options,
      }
    })
  }
}

@Module({
  imports: [
    AdminModule.createAdminAsync({
      imports: [DatabaseModule],
      useFactory: (prisma: PrismaService) => {
        const dmmf = (prisma as any)._baseDmmf as DMMFClass;

        return {
          adminJsOptions: {
            rootPath: '/admin',

            // updated here
            resources: new CResourceBuilder(prisma)
              .addResource('User')
              .addResource('Profile')
              .addResource('Post')
              .build(),
          },
          auth: {
            authenticate,
            cookieName: 'adminjs',
            cookiePassword: 'secret'
          },
          sessionOptions: {
            resave: true,
            saveUninitialized: true,
            secret: 'secret'
          },
        }
      },
      inject: [PrismaService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

```

> You can always export the classes, functions and objects from an external file.

### Mastering AdminJs

- [More options for resources](https://docs.adminjs.co/basics/resource)

- [Customizing your Admin Dashboard](https://docs.adminjs.co/ui-customization/writing-your-own-components)

## Conclusion

In conclusion, AdminJS is a powerful tool for creating admin dashboards for NestJS and Prisma applications. With its adapters for various ORM and ODM models, it simplifies the process of performing CRUD operations on models. It also provides customization options for the dashboard, making it easy to tailor it to specific project needs. By following the steps outlined in the article, developers can quickly set up AdminJS and start using it to manage their application's data.

Follow me on Twitter [@netrobeweb](https://twitter.com/netrobeweb) and [Hashnode](https://hashnode.com/@netrobe) where I post amazing projects and articles.

Thanks for reading, 😉.