Building Admin Panel with AdminJS and Node.js without any React or Tailwind code
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
Prerequisites: Knowledge of working with ORMs, Node.js, Prisma, and PostgreSQL.
Initializing Prisma: Commands to set up Prisma.
Connecting Prisma with PostgreSQL: Configuration in the
.env
file.Creating a Sample Schema: Defining tables like User, Products, Categories, etc.
Setting Up AdminJS: Integrating AdminJS with our Express app.
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.
Initialize Prisma
Set up your PostgreSQL database in the
.env
filenpm 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.
UI Customization - https://docs.adminjs.co/ui-customization/writing-your-own-components
Custom features - https://docs.adminjs.co/tutorials/content-management-system
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
andUploadCategories
.
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
. Thehandler
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! 🚀✨
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.