A complete demonstration of Power Apps Code Apps with Dataverse integration. This app showcases CRUD operations, lookup field resolution, Dataverse functions and actions, and modern React architecture patterns using PAC CLI generated services.
- CRUD Operations — Create, Read, Update, and Delete contacts in Dataverse
- Lookup Fields — Reading and writing relationships using
@odata.bindsyntax and efficient on-demand resolution - File Attachments — Uploading, downloading, and deleting files and images on Dataverse file/image columns
- Dataverse Functions & Actions — Three API patterns via generated services: unbound function (
WhoAmI), unbound action (SetAutoNumberSeed), and bound action (ConvertOwnerTeamToAccessTeam) - Three-Layer Architecture — UI components, custom hooks, generated services with clear separation
- Generated Services — Using PAC CLI auto-generated TypeScript services exclusively (no direct API calls)
- Error Handling — Try-catch at every layer with user-friendly messages and loading states
- Type Safety — Full TypeScript with auto-generated Dataverse models
- Node.js v22 or higher (Download)
- Power Platform CLI (
pac) (Install Guide) - Node CLI for Dataverse APIs (
npx power-appsfrom@microsoft/power-apps) — required for Dataverse functions/actions generation - Power Platform environment with Dataverse and Power Apps Code Apps enabled
pac auth create
pac env select --environment <your-environment-id>npm installpac code add-data-source -a dataverse -t contact
pac code add-data-source -a dataverse -t accountnpm run devOpen the Local Play URL in the same browser profile as your Power Platform tenant.
npm run build
pac code pushsrc/
├── components/ # UI components (presentation only, no business logic)
│ ├── Header.tsx
│ ├── ContactCard.tsx # Single contact card with lookup display
│ ├── ContactList.tsx # Grid of cards with New Contact button
│ ├── ContactForm.tsx # Create/edit form with lookup dropdown
│ ├── AccountList.tsx # Account list for file attachments tab
│ ├── AccountForm.tsx # Account form + file/image attachment sub-form
│ ├── ApiActionsPanel.tsx # Functions & Actions tab with three API patterns
│ └── ErrorMessage.tsx
│
├── hooks/ # Business logic and state management
│ ├── useContacts.ts # Contact CRUD operations and form state
│ ├── useAccounts.ts # Account data for dropdowns
│ ├── useAccountsCrud.ts # Full Account CRUD with file column select
│ ├── useCurrentUser.ts # Calls WhoAmI and returns current user identity
│ └── useLookupResolver.ts # Resolves lookup GUIDs to display names
│
├── generated/ # Auto-generated by PAC CLI — do not edit manually
│ ├── models/ # TypeScript entity types
│ └── services/ # Dataverse CRUD + API services
│ ├── WhoAmIService.ts # Unbound function
│ ├── SetAutoNumberSeedService.ts # Unbound action
│ └── ConvertOwnerTeamToAccessTeamService.ts # Bound action
│
├── App.tsx # Composition layer (thin — no business logic)
└── App.css # Application styles
The app demonstrates all four operations on the Dataverse contact table:
| Operation | Service Method | Notes |
|---|---|---|
| Create | ContactsService.create(data) |
Validate → prepare payload with OData bind for lookups |
| Read | ContactsService.getAll(options) |
select, orderBy, top for optimized queries |
| Update | ContactsService.update(id, data) |
Send changed fields; OData bind for lookup changes |
| Delete | ContactsService.delete(id) |
Confirmation → delete → reload list |
The app demonstrates both sides of Dataverse lookup relationships. See LOOKUPS.md for the full deep dive.
Writing a lookup (create/update):
// Link contact to account using OData bind syntax
contact['parentcustomerid_account@odata.bind'] = `/accounts(${accountId})`;
// Clear a lookup by setting it to null
updates['parentcustomerid_account@odata.bind'] = null;Reading a lookup (on-demand resolution):
// Step 1: Load contacts with lookup GUID fields
ContactsService.getAll({
select: ['contactid', 'firstname', '_msa_managingpartnerid_value']
});
// Step 2: Resolve the GUID to a display name when needed
AccountsService.get(contact._msa_managingpartnerid_value, {
select: ['accountid', 'name']
});All Dataverse access goes through PAC CLI generated services. Never use direct API calls.
const result = await ContactsService.getAll({
select: ['contactid', 'firstname', 'lastname'],
filter: 'statecode eq 0',
orderBy: ['createdon desc'],
top: 50,
});The File Attachments tab demonstrates file and image column operations on the account table:
| Operation | Service Method |
|---|---|
| Upload | AccountsService.upload(id, columnName, file, name) |
| Download file | AccountsService.downloadFile(id, columnName) |
| Download image | AccountsService.downloadImage(id, columnName, fullSize) |
| Delete | AccountsService.deleteFileOrImage(id, columnName) |
All methods return IOperationResult<T> — check result.success before using result.data.
The Functions & Actions tab demonstrates three API patterns. Use npx power-apps to discover and generate these services.
pac CLI does not currently generate Dataverse functions/actions services.
# Search for available actions and functions
npx power-apps find-dataverse-api --search "<name>"
# Generate a typed service
npx power-apps add-dataverse-api --api-name <OperationName>Not bound to any table. Takes no parameters. Returns data.
const result = await WhoAmIService.WhoAmI();
const user = result.data; // { UserId, BusinessUnitId, OrganizationId }Not bound to any table. Accepts typed scalar parameters. Performs a write — no data returned.
const result = await SetAutoNumberSeedService.SetAutoNumberSeed(
"contact", // EntityName
"cr123_num", // AttributeName
1000 // Value
);
// result.success === true on 204 No ContentBound to the team table. Operates on a specific record — the record GUID is passed as id.
const result = await ConvertOwnerTeamToAccessTeamService
.ConvertOwnerTeamToAccessTeam(teamId);
// result.success === true on 204 No Content- View contacts — App loads and displays all contacts on start
- Create — Click "New Contact", fill the form, click "Create Contact"
- Edit — Click any contact card to open it in the edit form
- Delete — Click "Delete" on a card and confirm the dialog
- Lookup — Use the "Managing Partner" dropdown to link contacts to accounts
- Select an account from the list (or create one)
- Scroll to the Attachments section
- Upload a file or image to the chosen column, download it back, or delete it
Opens automatically with the WhoAmI result displayed. The tab also shows the generated service signatures and call patterns for SetAutoNumberSeed (unbound action) and ConvertOwnerTeamToAccessTeam (bound action).
To add a new Dataverse table:
pac code add-data-source -a dataverse -t <table-logical-name>To add a Dataverse action or function:
# Note: This currently works with npx power-apps only (not pac CLI)
npx power-apps find-dataverse-api --search "<name>"
npx power-apps add-dataverse-api --api-name <OperationName>Both commands generate files in src/generated/. The generated services follow the same IOperationResult<T> pattern — create a hook wrapping the service call and pass the result down as props, following useCurrentUser.ts as a template.
| Issue | Solution |
|---|---|
| Authentication failed | Run pac auth create |
| Table not found | Ensure Contact and Account tables exist in your environment |
| Node version error | Run nvm use 22 |
| CORS errors | Access via the Local Play URL from the CLI output, not localhost directly |
| Build errors | Delete node_modules and run npm install again |
| File | Description |
|---|---|
| README.md | This file — overview, quick start, features, usage, and troubleshooting |
| ARCHITECTURE.md | Three-layer architecture, component hierarchy, data flow diagrams, event flows, and design patterns |
| LOOKUPS.md | Deep dive on lookup fields — naming conventions, reading GUIDs, writing with @odata.bind, and efficient on-demand resolution |
| DEVELOPMENT.md | Step-by-step guide to recreate this demo from scratch using the PAC CLI |
