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)
npx fastify-admin generate:resource PostThis creates src/entities/post.entity.ts and src/views/post.view.ts with starter boilerplate.
Step 2 — Edit the generated entity if needed
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
npx fastify-admin migrate:create add-postnpx fastify-admin migrate:upThat’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:
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:
import { ViewRegistry } from 'fastify-admin';import { PostView } from './post.view.js';
export const views = new ViewRegistry() .register('post', new PostView());Pass to the plugin:
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.
Related-Entity Tabs
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.
Sidebar Menu
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:
| Field | Type | Description |
|---|---|---|
name | string | Unique key used as the item identifier and for parent references |
label | string | Display text shown in the sidebar |
icon | string | Icon key resolved client-side via iconRegistry.registerEntityIcons() |
parent | string | Name of a parent item — nests this item under it as a collapsible group |
entity | string | Entity 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.
| Method | Path | Description |
|---|---|---|
GET | /api/{model}?page=1&limit=20 | Paginated list |
GET | /api/{model}/{id} | Show a single record |
POST | /api/{model}/create | Create a record |
PUT | /api/{model}/{id} | Update a record |
DELETE | /api/{model}/{id} | Delete a record |
Field Types
| MikroORM type | UI input |
|---|---|
string / varchar | Text input |
text | Textarea |
boolean | Checkbox |
number / integer | Number input |
Date / datetime | Date/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:
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?', }, ],})| Field | Type | Description |
|---|---|---|
label | string | Button text |
handler | (id, model) => Promise<void> | Called on click |
confirm | string | Optional confirmation prompt before running |
permission | string | Permission 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:
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.