Skip to content

Adding Entities

An “entity” is a data model — like a Post, Product, Order, or Customer. When you add one, the admin panel automatically generates a full CRUD interface for it (list, view, create, edit, delete).


The Simplest Case

Step 1 — Scaffold the entity and view (optional but recommended)

Terminal window
npx fastify-admin generate:resource Post

This creates src/entities/post.entity.ts and src/views/post.view.ts with starter boilerplate.

Step 2 — Edit the generated entity if needed

fastify-admin/src/entities/post.entity.ts
import { Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity()
export class Post {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property({ type: 'text' })
content!: string;
@Property({ default: false })
published = false;
}

Step 3 — Create and run a migration

Terminal window
npx fastify-admin migrate:create add-post
npx fastify-admin migrate:up

That’s it. Restart pnpm dev and you’ll see “Post” in the sidebar with a full list/create/edit/delete UI.


Customising How an Entity Looks

await app.register(fastifyAdmin, {
orm,
views: {
post: {
label: 'Blog Posts',
icon: 'Document',
list: {
columns: ['id', 'title', 'published'],
},
show: {
fields: ['id', 'title', 'content', 'published'],
},
edit: {
fields: ['title', 'content', 'published'],
},
add: {
fields: ['title', 'content'],
},
},
},
});

Disabling operations

Set any permission to false to disable it entirely for all users:

views: {
order: {
permissions: {
create: false,
edit: false,
delete: false,
},
},
}

Using the EntityView Class

Instead of an inline config object, you can create a dedicated file per entity by extending EntityView. This is the recommended approach for larger projects.

  • Directorysrc/
    • Directoryviews/
      • post.view.ts
      • product.view.ts
      • index.ts Collects all views into a ViewRegistry

Create a view class:

fastify-admin/src/views/post.view.ts
import { EntityView } from 'fastify-admin';
export class PostView extends EntityView {
label = 'Blog Posts';
icon = 'Document';
listColumns = ['id', 'title', 'published'];
showFields = ['id', 'title', 'content', 'published'];
editFields = ['title', 'content', 'published'];
addFields = ['title', 'content'];
// Optional: override the number of rows per page (default: 20)
pageSize = 50;
}

Register all views:

fastify-admin/src/views/index.ts
import { ViewRegistry } from 'fastify-admin';
import { PostView } from './post.view.js';
export const views = new ViewRegistry()
.register('post', new PostView());

Pass to the plugin:

dev.ts
import { views } from './views/index.js';
await app.register(fastifyAdmin, { orm, views });

Hiding an Entity from the Sidebar

post: {
sidebar: false,
}

Pagination

The list view is paginated server-side. The default page size is 20 rows. Override it per entity by implementing pageSize() on your EntityView:

export class PostView extends EntityView {
pageSize = 50;
}

The pagination bar appears automatically at the bottom of the list when the total number of records exceeds the page size. Clients pass ?page=1&limit=20 as query parameters — there is no request body for list reads.


The show page can display related collections as separate tabs. Tabs are only shown when relatedViews is explicitly configured — nothing is auto-detected.

export class UserView extends EntityView {
relatedViews = ['role'];
}

Each entry must be the entity model name (e.g. 'role', not the field name 'roles'). The tab label is derived from the model name automatically. Omit relatedViews entirely to suppress all relation tabs.


By default the sidebar lists all registered entities automatically. You can replace this with a fully controlled menu by passing menu to the plugin.

Each MenuItem has the following fields:

FieldTypeDescription
namestringUnique key used as the item identifier and for parent references
labelstringDisplay text shown in the sidebar
iconstringIcon key resolved client-side via iconRegistry.registerEntityIcons()
parentstringName of a parent item — nests this item under it as a collapsible group
entitystringEntity model name this item links to (routes to /{entity}/list)
await app.register(fastifyAdmin, {
orm,
menu: [
{ name: 'content', label: 'Content', icon: 'Folder' },
{ name: 'posts', label: 'Posts', entity: 'post', parent: 'content' },
{ name: 'products', label: 'Products', entity: 'product', icon: 'Package' },
],
})

Items without a parent are rendered as top-level entries. Items with a parent are grouped under that parent as a collapsible section.

Auto-loading the Security group

Set loadSecurity: true to automatically prepend a Security group containing all securityEntities (default: user, role, permission) at the top of the menu:

await app.register(fastifyAdmin, {
orm,
loadSecurity: true,
menu: [
// your other items — Security will appear above these
],
})

REST API Reference

All data operations use standard HTTP methods. Fields and columns are resolved server-side from the registry config — no request body is needed for reads.

MethodPathDescription
GET/api/{model}?page=1&limit=20Paginated list
GET/api/{model}/{id}Show a single record
POST/api/{model}/createCreate a record
PUT/api/{model}/{id}Update a record
DELETE/api/{model}/{id}Delete a record

Field Types

MikroORM typeUI input
string / varcharText input
textTextarea
booleanCheckbox
number / integerNumber input
Date / datetimeDate/time input
Relation (ManyToOne, ManyToMany)Select / multi-select

Relationships

MikroORM relations work automatically. For example, a Post that belongs to a Category:

@ManyToOne(() => Category)
category!: Category;

The edit form will show a dropdown of all categories.


Row Actions

Add custom buttons to every row in the list view. Each action gets the record’s id and model name.

// fastify-admin/src/views/post.view.ts (server)
import { EntityView } from 'fastify-admin'
export class PostView extends EntityView {
rowActions() {
return [
{
label: 'Publish',
href: '/api/posts/:id/publish', // :id is replaced at runtime
},
]
}
}

For client-side actions with custom logic, register them in main.tsx using entityRegistry:

apps/web/src/main.tsx
import { entityRegistry } from './lib/entityRegistry'
entityRegistry.register('post', {
actions: [
{
label: 'Publish',
handler: async (id) => {
await fetch(`/api/posts/${id}/publish`, { method: 'POST' })
},
confirm: 'Publish this post?',
},
],
})
FieldTypeDescription
labelstringButton text
handler(id, model) => Promise<void>Called on click
confirmstringOptional confirmation prompt before running
permissionstringPermission required to see the action. Defaults to {model}.action.{label}

Custom Cell Renderers

Override how a specific field value is displayed in the list table:

apps/web/src/main.tsx
import { entityRegistry } from './lib/entityRegistry'
entityRegistry.register('post', {
list: {
renderCell: (field, value) => {
if (field.name === 'published') {
return value ? '✅ Published' : '⬜ Draft'
}
},
},
})

Return undefined to fall back to the default renderer for that field.

Similarly, renderField customises the show page and renderInput customises the edit/create form:

entityRegistry.register('post', {
show: {
renderField: (field, value) => {
if (field.name === 'content') return <MarkdownPreview value={value} />
},
},
edit: {
renderInput: (field, value, onChange) => {
if (field.name === 'content') return <MarkdownEditor value={value} onChange={onChange} />
},
},
})

Full Component Override

Replace the entire list, show, or edit view with your own React component:

entityRegistry.register('post', {
list: {
component: ({ model, entity, records }) => (
<div>
{records.map(r => <div key={String(r.id)}>{String(r.title)}</div>)}
</div>
),
},
})

The component receives model (entity name string), entity (metadata), and records / record depending on the view type.