Building Admin Panel with AdminJS and Node.js without any React or Tailwind code

Sushil KumarSushil Kumar
10 min read

Admin panels play a crucial role in managing and maintaining web applications, but building one from scratch can often feel like a daunting task, especially when you’re trying to avoid the complexity of frontend frameworks like React, Tailwind, or Bootstrap. Fortunately, there's a simple and effective solution: AdminJS.

AdminJS makes it incredibly easy to create a fully functional and visually appealing admin panel with minimal effort and code. If you're curious about what you can accomplish with AdminJS, I highly recommend checking out this demo dashboard: https://adminjs-demo.herokuapp.com/app/login. Just click the "LOGIN" button directly on the login page to dive in.

Using AdminJS has been a game-changer for me, especially when it comes to visualizing the database. It's been invaluable for quickly creating dummy data and experimenting with different setups.

It literally just takes 5 minutes to create the Dashboard and then I can manually add some data in it, visualize it, edit it as many times as we go through production of our product.

Whether you're just getting started or looking to streamline your admin panel development, AdminJS offers a straightforward path to a professional-grade interface without the usual headaches.

AdminJs

AdminJS is compatible with a lot of frameworks and ORMs. You can check out the commands to download the plugins and adapters for your respective technology on this page. We will be using Express as the server and Prisma as an ORM for the PostgreSQL database, so use the following commands:

npm i adminjs
npm i @adminjs/express
npm i @adminjs/prisma

Overview of the Article

  1. Prerequisites: Knowledge of working with ORMs, Node.js, Prisma, and PostgreSQL.

  2. Initializing Prisma: Commands to set up Prisma.

  3. Connecting Prisma with PostgreSQL: Configuration in the .env file.

  4. Creating a Sample Schema: Defining tables like User, Products, Categories, etc.

  5. Setting Up AdminJS: Integrating AdminJS with our Express app.

  6. Customizing the Admin Panel: Adding features like media upload.

Prerequisites

To follow along with this article, you should have:

  • Basic knowledge of Node.js and Express.

  • Experience working with ORMs (Object-Relational Mappers) like Prisma.

  • Understanding of PostgreSQL and database schemas.

Initializing Prisma

We'll first initialize Prisma in our project.

  1. Initialize Prisma

  2. Set up your PostgreSQL database in the .env file

     npm install express prisma @prisma/client
     npx prisma init
    
     DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DATABASE"
    

Creating a Sample Schema

Let's create a sample schema for our blogging platform.

Define the schema in prisma/schema.prisma:

You can create whatever database you want. For this article, we are going to create some basic tables that we generally need for a blogging app like Users, Articles, and Categories.

Below is the sample schema that you may copy for testing purposes:

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

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

// Define User model
model User {
  id              Int             @id @default(autoincrement())
  name            String
  email           String          @unique
  countryCode     String?
  contact         String?
  SendAlerts  Boolean         @default(true)
  uuid            String          @unique
  password        String
  isVerified      Boolean         @default(true)
  createdAt       DateTime        @default(now())
  // Add other user-related fields as needed
  Bookmarks       Bookmarks[]
  subscriptions   subscriptions[]

  categories Category[] @relation("UserCategories")
}



model articles {
  id                   Int           @id @default(autoincrement())
  heading              String        @unique
  company              String?
  securityCode         String?
  title                String?       @unique
  category             String?
  description          String?
  summary              String?
  date                 DateTime?
  link                 String?
  imgUrl               String?
  negativeKeywordFound String?
  platform             String?
  createdAt            DateTime      @default(now())
  updatedAt            DateTime      @default(now()) @updatedAt
  // Add other fields as needed
  Bookmarks Bookmarks[]
}

model Bookmarks {
  id         Int       @id @default(autoincrement())
  user       User      @relation(fields: [userId], references: [id])
  userId     Int
  article    articles  @relation(fields: [articleId], references: [id])
  articleId  Int

  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // @@unique([userId, newsId])
}

model Category {
  id        Int      @id @default(autoincrement())
  name      String
  popular   Boolean  @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  // Add other fields as needed
  users            User[]             @relation("UserCategories")
}

model subscriptions {
  id              Int       @id @default(autoincrement())
  user            User      @relation(fields: [userId], references: [id])
  userId          Int       @unique
  plan_id            String?
  subscription_id String?
  customer_id     String    @unique
  status          String?
  startDate       DateTime?
  endDate         DateTime?
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @default(now()) @updatedAt
}

After we have got our schema defined, run the following command to migrate it to our database (which will create the tables defined in the schema):

npx prisma migrate dev

Setting Up AdminJS

Installing AdminJS and Required Adapter

npm install adminjs @adminjs/express @adminjs/prisma

Integrating AdminJS with Express

We do not need to write much code for this. We first need to import the dependencies, add the name of tables from your schema you want in the Admin Panel in the resource of adminOptions Array which we pass while initialising the Admin JS.

import AdminJS from 'adminjs'
import * as url from 'url'
import AdminJSExpress from '@adminjs/express'
import express from 'express'
import * as AdminJSPrisma from '@adminjs/prisma'
import { PrismaClient } from '@prisma/client'
import { getModelByName } from "@adminjs/prisma";
import cors from 'cors'
import session from "express-session"; // for authetication of Admin
import Connect from "connect-pg-simple"; // for authetication of Admin
import bodyParser from 'body-parser';

const app = express();
const PORT = process.env.PORT;
const HOST = process.env.HOST;
const prisma = new PrismaClient();

app.use(cors());
app.use(express.static("public"));
app.use('/public', express.static(path.join(currFolder, 'public')));

Now, let get into the coding section which has 3 main sections:

  • where we first create an adapter to connect our Database and schema to AdminJS package.

  • Then we define the resources (database models) to show under adminOptions. AdminOptions is the main crux, this is the main object where we make any kind of customisation to our Panel.

  • Add authentication and add initialise the Admin JS instance

// Admin Credenmtials to log into Admin Panel (can store them in DB as well)
const DEFAULT_ADMIN = {
    email: process.env.DEFAULT_EMAIL,
    password: process.env.DEFAULT_PASSWORD,
};

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

app.use(
    session({
        secret: process.env.JWT_SECRET_KEY,
        resave: false,
        saveUninitialized: true,
    })
);

AdminJS.registerAdapter({
    Resource: AdminJSPrisma.Resource,
    Database: AdminJSPrisma.Database,
});

// add the name of tables from your schema you want in the Admin Panel in the resource of adminOptions Array which we pass while initializing the Adminn JS
const adminOptions = {
    resources: [
        {
            resource: { model: getModelByName("User"), client: prisma },
            options: {},
        },
        {
            resource: { model: getModelByName("articles"), client: prisma },
            options: {},
        },
        {
            resource: { model: getModelByName("Bookmarks"), client: prisma },
            options: {},
        },
        {
            resource: { model: getModelByName("Category"), client: prisma },
            options: {},
        },

    ],
    rootPath: "/admin",
};

const admin = new AdminJS(adminOptions);

await admin.watch();
const ConnectSession = Connect(session);
const sessionStore = new ConnectSession({
    conObject: {
        connectionString: process.env.DATABASE_URL,
        ssl: process.env.NODE_ENV === "production",
    },
    tableName: "session",
    createTableIfMissing: true,
});

const adminRouter = AdminJSExpress.buildAuthenticatedRouter(
    admin,
    {
        authenticate,
        cookieName: process.env.ADMINJS_COOKIE_NAME,
        cookiePassword: process.env.ADMINJS_COOKIE_PASSWORD,
    },
    null,
    {
        store: sessionStore,
        resave: true,
        saveUninitialized: true,
        secret: process.env.ADMINJS_COOKIE_SECRET,
        cookie: {
            httpOnly: process.env.NODE_ENV === "production",
            secure: process.env.NODE_ENV === "production",
        },
        name: "adminjs",
    }
);

app.use(admin.options.rootPath, adminRouter);

app.use(bodyParser.json({
    verify: (req, res, buf) => {
        req.rawBody = buf;
    }
})); 
// Make sure to add bosy parser after Admin Options to
// req.rawBody = buf;
// This line stores the raw, unparsed request body in req.rawBody. This can be useful in cases where you need to retain the original, unaltered body data
// we need this in some customization cases like file parsing in Admin Panel

app.listen(PORT, () => {
    console.log(
        `AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`
    );
});

Customizing Dashboard in AdminJS

AdminJS is incredibly flexible, allowing for extensive customization beyond its built-in CRUD operations, search, sort, pagination, and filtering. You can modify the UI, upload files, execute custom functions, create graphs, and more. To make these changes or add new components, the AdminOptions configuration is key. Below is an overview of how you can start customising the AdminJS panel, including links to more detailed documentation.
Lets see ho we can do that. To make any changes or adding new components, AdminOptions is all we need. So you can learn more about customising the paNEL from docs better as I can not cover everything in just 1 article. But I will give an over view and how you may start.

Uploading Files

Component Loader Setup

To customize AdminJS, you need to load your custom components first. This is done using the ComponentLoader class provided by AdminJS. Here's how you set it up:

// component-loader.js
import { ComponentLoader } from "adminjs";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const componentLoader = new ComponentLoader();

const Components = {
    UploadNews: componentLoader.add(
        "UploadNews",
        path.resolve(__dirname, "./UploadNews.jsx")
    ),
    UploadCategories: componentLoader.add(
        "UploadCategories",
        path.resolve(__dirname, "./UploadCategories.jsx")
    ),
};
// console.log(Components.Upload);

export { componentLoader, Components };

Explanation:

  • ComponentLoader: This class is responsible for loading and registering custom React components to be used within the AdminJS interface.

  • path and fileURLToPath: These are used to resolve the correct paths for the component files.

  • Components: This object stores references to the custom components you’ve added, such as UploadNews and UploadCategories.

Creating the Upload Component

Next, let's create a custom component for uploading articles in CSV format:

//upload-article.js
import React, { useState } from 'react'
import { ApiClient } from "adminjs"
import { DropZone, Loader, Button } from '@adminjs/design-system'
import csvtojson from "csvtojson";
import axios from 'axios';


const api = new ApiClient()

function UploadArticles(props) {
    const [file, setFile] = useState(null)
    const [isLoading, setIsLoading] = useState(false)

    const onUpload = async (file) => {
        // save the file in current folder
        console.log(file, "file");
        console.log(process.env.WEB_URL);

        setFile(file)
    }

    const uploadHandler = async () => {
        try {
            if (file.length === 0) {
                return;
            }



            let curFile = file[0];
            console.log(curFile, "curFile");

            const reader = new FileReader();
            reader.onload = async (e) => {

                const text = e.target.result;
                let jsonData = await csvtojson().fromString(text);
                console.log(jsonData, "jsonData");

                const fileName = curFile.name;

                let result = await api.resourceAction({
                    resourceId: 'articles',
                    actionName: "UploadArticles",
                    data: {
                        fileName: fileName,
                        newsData: jsonData
                    }
                })
                if (result.status) {
                    window.location = props.resource.href;
                } else {
                    alert("Error while uploading users");
                }
            };
            reader.readAsText(curFile);
        } catch (e) {
            console.log(e);
        }
    }

    return (
        <React.Fragment>
            {
                isLoading ? <Loader /> : <div style={{
                    display: "flex",
                    flexDirection: "column",
                }}>
                    <DropZone style={{
                        width: "100%"
                    }} multiple={false} onChange={onUpload} validate={{
                        mimeTypes: ["text/csv"]
                    }}></DropZone>
                    <Button style={{
                        margin: "auto",
                        marginTop: "20px"
                    }} variant="primary" onClick={uploadHandler}>Save</Button>
                </div>
            }
        </React.Fragment>
    )
}

export default UploadArticles;

Explanation:

  • State Management: useState is used to manage the file selected by the user and the loading state.

  • DropZone: A component from the AdminJS design system that allows users to drag and drop files.

  • FileReader: Used to read the contents of the uploaded file and convert it from CSV to JSON using csvtojson.

  • ApiClient: This is an instance of the AdminJS API client, used to communicate with AdminJS resources.

  • uploadHandler: This function handles the file upload process, converting the CSV file into JSON format and sending it to the server.

Integrating the Component into AdminJS

Finally, we integrate our custom upload component into the AdminJS configuration:

import { componentLoader, Components } from './admin/component-loader.js'

const adminOptions = {
    componentLoader,
    resources: [
        {
            resource: { model: getModelByName("articles"), client: prisma },
            options: {
                actions: {
                    UploadNews: {
                        actionType: "resource",
                        label: "Upload Articles",
                        component: Components.UploadArticles,
                        handler: async (request, response, context) => {

                            console.log(request.payload, "REQUEST PAYLOAD");
                            let result = processNewsCSV(request.payload);

                            return {
                                status: result,
                            };
                        },
                    },
                },
                features: [
                    uploadFeature({
                        componentLoader,
                        provider: {
                            aws: awsProvider,
                        },
                        multiple: true,
                        properties: {
                            key: "file",
                            bucket: "bucket",
                            mimeTypes: ["text/csv", "application/pdf"],
                        },
                    })
                ]
            },
        },
      ],
    rootPath: "/admin"
    };

Explanation:

  • actions: This defines custom actions for the resource, such as UploadArticles. The handler function processes the request and handles the CSV data upload.

  • uploadFeature: This feature integrates file upload capabilities into AdminJS, allowing files to be uploaded to an AWS S3 bucket.

  • componentLoader: This is passed to ensure that AdminJS can load the custom components we created.

By following the above steps, you can start customizing your AdminJS dashboard to fit your needs. Whether it’s changing the UI, uploading files, or running custom functions, AdminJS provides the flexibility you need. You can explore more in the AdminJS documentation to fully leverage its capabilities.

Conclusion

AdminJS makes creating an admin panel incredibly simple and efficient, eliminating the need for extensive frontend work. With its powerful customization options and seamless integration with Node.js, Express, and Prisma, you can build a robust admin interface in no time. If you need any help or have questions, feel free to ask in the comments, and I'll be happy to assist! 🚀✨

14
Subscribe to my newsletter

Read articles from Sushil Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sushil Kumar
Sushil Kumar

Hey devs, I am a Fullstack developer from India. Working for over 3 years now. I love working remote and exploring new realms of technology. Currently exploring AI and building scalable systems.