@alsocoder/apna-crud
v0.1.1
Published
Auto CRUD pages using ApnaTable, ApnaForm, ApnaModal, and ApnaToast.
Readme
@alsocoder/apna-crud
Auto CRUD pages built on ApnaTable, ApnaForm, ApnaModal, and ApnaToast. Drop a single <ApnaCrud /> into your admin panel (e.g. @alsocoder/apna-panel) for full list/create/edit/delete flows.
Install
npm install @alsocoder/apna-crudFor formMode="page", also install react-router-dom.
ApnaCrud bundles table, form, modal, and toast styles — one CSS import:
import "@alsocoder/apna-crud/styles.css"Optional (for type: "media" fields):
npm install @alsocoder/apna-media-libraryQuick start
REST API
<ApnaCrud
name="users"
title="Users"
endpoint="/api/users"
baseUrl={import.meta.env.VITE_API_URL}
columns={[
{ id: "name", header: "Name", accessorKey: "name" },
{ id: "email", header: "Email", accessorKey: "email" },
]}
formFields={[
{ type: "input", name: "name", label: "Name", props: { required: true } },
{ type: "input", name: "email", label: "Email", props: { format: "email", required: true } },
]}
permissions={{
list: "users.view",
create: "users.create",
update: "users.update",
delete: "users.delete",
}}
features={["bulk"]}
/>Custom functions
<ApnaCrud
name="users"
title="Users"
list={listUsers}
create={createUser}
update={updateUser}
delete={deleteUser}
get={getUser}
columns={[...]}
formFields={[...]}
/>Inside ApnaPanel
import { ApnaPanelRoute } from "@alsocoder/apna-panel-react-router"
import { ApnaCrud } from "@alsocoder/apna-crud"
import "@alsocoder/apna-crud/styles.css"
<Route
path="/users/*"
element={
<ApnaPanelRoute permissions="users.view" fallback={<ApnaPanelForbidden />}>
<ApnaCrud
formMode="page"
basePath="/users"
name="users"
title="Users"
{...usersResource}
/>
</ApnaPanelRoute>
}
/>ApnaCrud props — full reference
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| name | string | required | Resource name (used for routes, data-resource attr) |
| title | string | — | Page/table title |
| endpoint | string \| ApnaCrudEndpoints | — | REST endpoint(s) |
| baseUrl | string | — | API base URL (with endpoint) |
| list | fn | — | (params) => { items, total } |
| create | fn | — | (data) => record |
| update | fn | — | (id, data) => record |
| delete | fn | — | (id) => void |
| get | fn | — | (id) => record — required for page mode direct URL |
| columns | ApnaTableColumn[] | auto | Table columns |
| formFields | ApnaFormFieldConfig[] | — | Create/edit form fields |
| formColumns | ApnaFormColumns | — | Responsive form grid { default: 1, md: 2, lg: 3 } |
| formProps | ApnaCrudFormProps | — | Forwarded to ApnaFormBuilder (see below) |
| formMode | "modal" \| "drawer" \| "page" | "modal" | How create/edit forms open |
| basePath | string | /${name} | Base path for page mode |
| routes | { create?, edit? } | — | Custom route segments for page mode. Default create segment is create; /add is also accepted as an alias unless routes.create is customized. |
| overlaySize | "sm" \| "md" \| "lg" \| "xl" | "md" | Modal/drawer size |
| permissions | ApnaCrudPermissions | — | list, create, update, delete permission keys |
| features | string[] | — | "bulk", "export", "import", "archive", "duplicate", "softDelete" |
| idKey | string | "id" | Record id field name |
| getRowId | fn | — | Custom row id extractor |
| transformCreate | fn | — | Transform payload before create |
| transformUpdate | fn | — | Transform payload before update |
| onSuccess | fn | — | (action, record?) => void |
| onError | fn | — | (error, action) => void |
| className | string | — | Root wrapper class |
| emptyMessage | string | — | Empty table message |
Form modes
Modal (default)
Create/edit opens in a centered modal.
<ApnaCrud
formMode="modal"
overlaySize="md"
name="products"
title="Products"
formFields={[...]}
{...api}
/>Drawer
Create/edit opens in a right-side drawer.
<ApnaCrud
formMode="drawer"
overlaySize="lg"
name="orders"
title="Orders"
formFields={[...]}
{...api}
/>Page (route-based)
Full-page forms at separate routes. Mount under a splat route:
// router
<Route path="/users/*" element={<UsersCrudPage />} />
function UsersCrudPage() {
return (
<ApnaCrud
formMode="page"
basePath="/users"
name="users"
title="Users"
get={getUser} // required for direct /users/:id/edit access
list={listUsers}
create={createUser}
update={updateUser}
delete={deleteUser}
columns={[...]}
formFields={[...]}
/>
)
}Auto-created routes:
| URL | Screen |
|-----|--------|
| /users | Table list |
| /users/create | Create form |
| /users/add | Create form (alias when default create segment is used) |
| /users/:id/edit | Edit form |
Custom route segments:
routes={{ create: "new", edit: ":id" }}
// → /users/new and /users/:id/edit only (no /add alias when create is customized)Data sources
REST with createRestCrud
import { ApnaCrud, createRestCrud } from "@alsocoder/apna-crud"
const usersApi = createRestCrud<User>(import.meta.env.VITE_API_URL, "/api/users")
<ApnaCrud
name="users"
{...usersApi}
columns={[...]}
formFields={[...]}
/>Custom endpoints object
endpoint={{
list: "/api/users",
create: "/api/users",
update: "/api/users/:id",
delete: "/api/users/:id",
get: "/api/users/:id",
}}
baseUrl="https://api.example.com"defineCrudResource (reusable config)
import { defineCrudResource } from "@alsocoder/apna-crud"
export const usersResource = defineCrudResource({
name: "users",
title: "Users",
columns: [
{ id: "name", header: "Name", accessorKey: "name" },
{ id: "email", header: "Email", accessorKey: "email" },
],
formFields: [
{ type: "input", name: "name", label: "Name" },
{ type: "input", name: "email", label: "Email", props: { format: "email" } },
],
formColumns: { default: 1, md: 2 },
permissions: {
list: "users.view",
create: "users.create",
update: "users.update",
delete: "users.delete",
},
features: ["bulk"],
...usersApi,
})
// usage
<ApnaCrud {...usersResource} formMode="modal" />Forms (formFields + formProps)
Forms are rendered by ApnaFormBuilder from @alsocoder/apna-form. ApnaCrud handles table, modal/drawer/page shell, and API calls.
Basic fields
formFields={[
{ type: "input", name: "title", label: "Title", colSpan: { md: 8 }, props: { required: true } },
{ type: "select", name: "status", label: "Status", colSpan: { md: 4 }, props: { options, placeholder: "Select" } },
{ type: "textarea", name: "notes", label: "Notes", props: { rows: 4 } },
{ type: "date", name: "dueDate", label: "Due date" },
{ type: "checkbox", name: "active", label: "Active" },
]}
formColumns={{ default: 1, md: 2, lg: 3 }}Advanced fields
formFields={[
{ type: "editor", name: "body", label: "Content" },
{ type: "editor", name: "html", label: "HTML", props: { enableHtmlMode: true } },
{ type: "upload", name: "resume", label: "Resume", props: { accept: ".pdf", maxSize: 5 * 1024 * 1024 } },
{ type: "media", name: "cover", label: "Cover", props: { folder: "blog/covers" } },
]}formProps (forwarded to ApnaFormBuilder)
Everything from ApnaFormBuilderProps except config, columns, children, onSubmit, className, classNames:
import type { ApnaUploadFn } from "@alsocoder/apna-upload"
import { z } from "zod"
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
const uploadFile: ApnaUploadFn = async (file, { signal, onProgress }) => {
const formData = new FormData()
formData.append("file", file)
onProgress(30)
const res = await fetch("/api/upload", { method: "POST", body: formData, signal })
onProgress(100)
return res.json()
}
<ApnaCrud
formFields={[...]}
formProps={{
schema, // Zod / validation schema
mode: "onBlur", // validation mode
upload: uploadFile, // global upload handler
transformUploadResult: (r) => r.path, // normalize upload response
defaultUploadProps: {
accept: "image/*",
maxSize: 5 * 1024 * 1024,
},
}}
{...resource}
/>Media picker setup
For type: "media" fields, wrap your app with ApnaMediaProvider once at the app level:
npm install @alsocoder/apna-media-libraryimport { ApnaMediaProvider } from "@alsocoder/apna-media-library"
import "@alsocoder/apna-media-library/styles.css"
<ApnaMediaProvider adapter={mediaAdapter}>
<ApnaPanelRouterProvider config={config}>
<ApnaPanelShell>
<Routes>...</Routes>
</ApnaPanelShell>
</ApnaPanelRouterProvider>
</ApnaMediaProvider>Pass formProps.upload for type: "upload" fields. See ApnaForm README for full field docs.
Permissions
permissions={{
list: "users.view", // hide table / page
create: "users.create", // hide create button
update: "users.update", // hide edit action
delete: "users.delete", // hide delete action
}}Wire to your panel RBAC — ApnaCrud checks permission strings you provide. Use the same keys as @alsocoder/apna-panel rolePermissions.
Features
features={["bulk", "export", "import", "archive", "duplicate", "softDelete"]}| Feature | Description |
|---------|-------------|
| bulk | Multi-select + bulk delete |
| export | Export table data |
| import | Import data |
| archive | Archive records |
| duplicate | Duplicate row |
| softDelete | Soft delete instead of hard delete |
Callbacks & transforms
<ApnaCrud
transformCreate={(data) => ({ ...data, createdBy: currentUser.id })}
transformUpdate={(data) => ({ ...data, updatedAt: new Date().toISOString() })}
onSuccess={(action, record) => {
if (action === "create") toast.success(`Created ${record?.name}`)
}}
onError={(error, action) => {
toast.error(`${action} failed: ${error.message}`)
}}
{...resource}
/>Complete examples
1. Modal CRUD (products)
<ApnaCrud
name="products"
title="Products"
formMode="modal"
overlaySize="md"
formColumns={{ default: 1, md: 2 }}
columns={[
{ id: "name", header: "Product", accessorKey: "name" },
{ id: "sku", header: "SKU", accessorKey: "sku" },
{ id: "price", header: "Price", accessorKey: "price" },
{ id: "status", header: "Status", accessorKey: "status" },
]}
formFields={[
{ type: "input", name: "name", label: "Product name", colSpan: { md: 6 }, props: { required: true } },
{ type: "input", name: "sku", label: "SKU", colSpan: { md: 6 }, props: { required: true } },
{ type: "input", name: "price", label: "Price", colSpan: { md: 4 }, props: { format: "number" } },
{ type: "select", name: "status", label: "Status", colSpan: { md: 4 }, props: { options: statusOptions } },
]}
permissions={{ list: "products.view", create: "products.create", update: "products.update", delete: "products.delete" }}
features={["bulk"]}
list={listProducts}
create={createProduct}
update={updateProduct}
delete={deleteProduct}
/>2. Drawer CRUD (orders)
<ApnaCrud
name="orders"
title="Orders"
formMode="drawer"
overlaySize="lg"
columns={[...]}
formFields={[...]}
{...ordersApi}
/>3. Page CRUD with large form (projects)
<ApnaCrud
name="projects"
title="Projects"
formMode="page"
basePath="/crud/projects"
formColumns={{ default: 1, md: 2, lg: 3 }}
columns={[
{ id: "title", header: "Project", accessorKey: "title" },
{ id: "client", header: "Client", accessorKey: "client" },
{ id: "status", header: "Status", accessorKey: "status" },
]}
formFields={[
{ type: "input", name: "title", label: "Title", colSpan: { md: 8 } },
{ type: "media", name: "coverImage", label: "Cover", colSpan: { md: 6 }, props: { folder: "covers" } },
{ type: "upload", name: "contract", label: "Contract", props: { accept: ".pdf" } },
{ type: "editor", name: "brief", label: "Brief" },
{ type: "editor", name: "contentHtml", label: "Scope", props: { enableHtmlMode: true } },
]}
formProps={{
upload: uploadFile,
defaultUploadProps: { accept: "image/*,.pdf", maxSize: 10 * 1024 * 1024 },
}}
get={getProject}
list={listProjects}
create={createProject}
update={updateProject}
delete={deleteProject}
features={["bulk"]}
/>4. REST API one-liner
import { ApnaCrud, createRestCrud, defineCrudResource } from "@alsocoder/apna-crud"
const api = createRestCrud<Product>("https://api.example.com", "/products")
export const productsResource = defineCrudResource({
name: "products",
title: "Products",
columns: [...],
formFields: [...],
...api,
})
<ApnaCrud {...productsResource} baseUrl="https://api.example.com" endpoint="/products" />Hooks & utilities
import { useApnaCrud, createRestCrud, fetchJson, defineCrudResource } from "@alsocoder/apna-crud"
import { ApnaCrudPageRoutes, getApnaCrudBasePath } from "@alsocoder/apna-crud"| Export | Description |
|--------|-------------|
| useApnaCrud(config, baseUrl?) | Headless CRUD state (advanced) |
| createRestCrud(baseUrl, endpoint) | REST list/create/update/delete/get helpers |
| fetchJson(url, options?) | JSON fetch with error handling |
| defineCrudResource(config) | Type-safe resource config builder |
| ApnaCrudPageRoutes | Low-level page mode router (used internally) |
| getApnaCrudBasePath(config) | Resolve base path for page mode |
ApnaPanel + ApnaCrud full wiring
// panel config menu
menu: defineMenu([
{ id: "dashboard", label: "Dashboard", href: "/" },
{ id: "products", label: "Products", href: "/crud/products", permission: "products.view" },
{ id: "users", label: "Users", href: "/crud/users", permission: "users.view" },
])
// routes
<Route path="/" element={<ApnaPanelRoute permissions="dashboard.view"><ApnaPanelHomePage /></ApnaPanelRoute>} />
<Route path="/crud/products" element={<ApnaPanelRoute permissions="products.view"><ProductsPage /></ApnaPanelRoute>} />
<Route path="/crud/users/*" element={<ApnaPanelRoute permissions="users.view"><UsersPage /></ApnaPanelRoute>} />
// products page — modal mode
function ProductsPage() {
return <ApnaCrud formMode="modal" {...productsResource} formProps={sharedFormProps} />
}
// users page — page mode (needs splat route)
function UsersPage() {
return <ApnaCrud formMode="page" basePath="/crud/users" {...usersResource} formProps={sharedFormProps} />
}Shared formProps (upload, schema) at page or app level — set once, pass to every <ApnaCrud />.
Playground demos
See ApnaPanel/playground for live examples:
| Demo | formMode | Features |
|------|------------|----------|
| Products | modal | Basic fields, bulk delete |
| Orders | drawer | Drawer overlay |
| Users | page | Route-based forms |
| Projects | page | Editor, upload, media, 18+ fields |
cd ApnaPanel && npm run dev:playgroundLicense
MIT
