Skip to main content

Step 6: Add inventory forms

The last piece is a create/edit workflow that reuses the same validators and API you built earlier.

1. Create form definitions

mkdir -p apps/saasframe/src/modules/inventory/backend/inventory/create
touch apps/saasframe/src/modules/inventory/backend/inventory/create/page.tsx
touch apps/saasframe/src/modules/inventory/backend/inventory/create/page.meta.ts
apps/saasframe/src/modules/inventory/backend/inventory/create/page.tsx
import { CrudForm } from '@saasframe/ui/backend/CrudForm';
import type { CrudFormSchema } from '@saasframe/ui/backend/CrudForm/types';
import { readApiResultOrThrow } from '@saasframe/ui/backend/utils/apiCall';
import type { UpsertInventoryItemInput } from '../../data/validators';

const schema: CrudFormSchema<UpsertInventoryItemInput> = {
title: 'Create Inventory Item',
fields: [
{ id: 'sku', label: 'SKU', component: 'text', required: true },
{ id: 'name', label: 'Name', component: 'text', required: true },
{ id: 'quantity', label: 'Quantity', component: 'number', required: true, min: 0 },
{ id: 'location', label: 'Location', component: 'text' },
],
};

async function submit(data: UpsertInventoryItemInput) {
return readApiResultOrThrow('/api/items', {
method: 'POST',
body: JSON.stringify(data),
}, { errorMessage: 'Failed to create inventory item' });
}

const InventoryCreatePage = () => {
return <CrudForm schema={schema} onSubmit={submit} redirectTo="/backend/inventory/list" />;
};

export default InventoryCreatePage;
apps/saasframe/src/modules/inventory/backend/inventory/create/page.meta.ts
import type { PageMetadata } from '@saasframe/shared/modules/registry';

export const metadata: PageMetadata = {
title: 'Add item',
group: 'Operations',
order: 21,
requireAuth: true,
requireFeatures: ['inventory.create'],
};

2. Reuse the form for edits

Create an edit route that fetches row data and submits via PATCH:

mkdir -p apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit
touch apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.tsx
touch apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.meta.ts
apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.tsx
import { CrudForm } from '@saasframe/ui/backend/CrudForm';
import type { CrudFormSchema } from '@saasframe/ui/backend/CrudForm/types';
import { readApiResultOrThrow } from '@saasframe/ui/backend/utils/apiCall';
import type { UpsertInventoryItemInput } from '../../../data/validators';

const schema: CrudFormSchema<UpsertInventoryItemInput> = {
title: 'Edit Inventory Item',
fields: [
{ id: 'sku', label: 'SKU', component: 'text', required: true },
{ id: 'name', label: 'Name', component: 'text', required: true },
{ id: 'quantity', label: 'Quantity', component: 'number', required: true, min: 0 },
{ id: 'location', label: 'Location', component: 'text' },
],
};

async function fetchItem(id: string) {
return readApiResultOrThrow(`/api/items/${id}`, undefined, { errorMessage: 'Inventory item not found' });
}

async function submit(id: string, data: UpsertInventoryItemInput) {
return readApiResultOrThrow(`/api/items/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}, { errorMessage: 'Failed to update inventory item' });
}

type Props = { params: { id: string } };

const InventoryEditPage = async ({ params }: Props) => {
const item = await fetchItem(params.id);
return (
<CrudForm
schema={schema}
initialValues={item}
onSubmit={(data) => submit(params.id, data)}
redirectTo="/backend/inventory/list"
/>
);
};

export default InventoryEditPage;
apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.meta.ts
import type { PageMetadata } from '@saasframe/shared/modules/registry';

export const metadata: PageMetadata = {
title: 'Edit item',
group: 'Operations',
order: 22,
requireAuth: true,
requireFeatures: ['inventory.edit'],
};

Update the list page to surface "Create" and "Edit" buttons -- either via DataTable row actions or links in your JSX. Users with the correct features can now manage inventory end to end.

After adding the new pages, regenerate:

yarn generate
yarn saasframe configs cache structural --all-tenants

That completes the tutorial sequence. You now have a fully working inventory module with:

  • Module metadata, RBAC features, and default role assignments (index.ts, acl.ts, setup.ts)
  • A MikroORM entity with multi-tenant scoping and soft deletes (data/entities.ts)
  • Input validation with zod (data/validators.ts)
  • A REST API powered by the CRUD factory (api/items/route.ts)
  • A data table list page (backend/inventory/list/page.tsx)
  • Create and edit forms (backend/inventory/create/page.tsx, backend/inventory/[id]/edit/page.tsx)

Next steps

In the rest of the documentation you will dive deeper into the framework primitives that power these steps: