The future is agentic.

Tutorials

Build your first custom dashboard app in minutes.

October 3, 2025 Giuseppe Ciotola

In this example, we will create a generic app that lists some resources: customers, markets, shipments, and SKUs. From the list, you will be able to click through to the detail page of each item. It is a simple but powerful way to understand the structure of a custom app and prepare for more advanced scenarios.

What you need before starting

To follow along, make sure you have:

  • A Commerce Layer organization with some existing data (markets, customers, or SKUs).
  • Node.js with pnpm installed.
  • Basic knowledge of React and TypeScript.
Step 1

Create your repository from the template

We maintain a repository called dashboard-apps, which hosts the current dashboard apps released as open source project. Use it as a template to create your own repository by leveraging GitHub’s Create from template tool:

Once your repository is created, install the dependencies with the command below:

pnpm install

Inside the /apps folder, you will find a starter template called my_sample_app, which will serve as the foundation for our project alongside the other available apps.

Step 2

Set up your app

The starter already includes the essentials as you can see checking the minimal structure provided withing the my_sample_app folder. Its entry point (main.tsx) mounts the TokenProvider component, which manages authentication and reads the access token. The provider is already configured with kind="generic". This allows the app to fetch multiple types of resources instead of being restricted to just one set of permissions (as explain in the previous article):

You are free to rename the app folder and change the appSlug value. This slug defines the URL path where your app will be accessible, in this example:

https://your-domain/my_sample_app
Step 3

Add routes and pages

Next we will configure navigation. Open /src/data/routes.ts and add the routes for your application. Besides the default home page, you will want to include a list view and a details view.

The starter app, along with all other apps, uses wouter for routing, but you are free to replace it with any library of your choice.

Step 4

Create the pages

In the /src/pages folder, create a new file for each of the routes you just defined and import them into App.tsx:

For the home page we will keep things simple. Using components like HomePageLayout, Spacer, List, and ListItem you can display a grid of resource types such as customers, markets, shipments, and SKUs.

If you want to dive deeper into the components we used, check out the App Elements library documentation. It is packed with examples and explanations.

import { appRoutes } from '#data/routes'
import {
  formatResourceName,
  HomePageLayout,
  Icon,
  List,
  ListItem,
  Spacer
} from '@commercelayer/app-elements'
import type { ListableResourceType } from '@commercelayer/sdk'
import type { FC } from 'react'
import { useLocation } from 'wouter'

const resources: ListableResourceType[] = [
  'customers',
  'markets',
  'shipments',
  'skus'
]

const Page: FC = () => {
  const [, setLocation] = useLocation()
  return (
    <HomePageLayout title='My Sample App'>
      <Spacer top='14'>
        <List title='Resources'>
          {resources.map((resourceType) => (
            <ListItem
              key={resourceType}
              onClick={() => {
                setLocation(appRoutes.list.makePath({ resourceType }))
              }}
            >
              {formatResourceName({
                resource: resourceType,
                format: 'title',
                count: 'plural'
              })}
              <Icon name='caretRight' />
            </ListItem>
          ))}
        </List>
      </Spacer>
    </HomePageLayout>
  )
}

export default Page

The list page goes one step further. For this page, we will combine simple UI elements like PageLayout and ListItem with the useResourceList hook, which takes care of fetching data and even supports infinite scrolling out of the box.

import { appRoutes } from '#data/routes'
import {
  Icon,
  ListItem,
  PageLayout,
  type PageProps,
  Text,
  formatResourceName,
  useResourceList
} from '@commercelayer/app-elements'
import type { FC } from 'react'
import { useLocation } from 'wouter'

const Page: FC<PageProps<typeof appRoutes.list>> = ({ params }) => {
  const { resourceType } = params
  const [, setLocation] = useLocation()
  const { ResourceList, isLoading } = useResourceList({
    type: resourceType,
    query: {
      pageSize: 25
    }
  })

  if (isLoading) {
    return null
  }

  return (
    <PageLayout
      title={formatResourceName({
        resource: resourceType,
        format: 'title',
        count: 'plural'
      })}
      navigationButton={{
        label: 'Select type',
        onClick: () => {
          setLocation(appRoutes.home.makePath({}))
        }
      }}
    >
      <ResourceList
        title='All'
        ItemTemplate={({ resource }) => {
          if (resource == null) {
            return null
          }
          return (
            <ListItem
              onClick={() => {
                setLocation(
                  appRoutes.details.makePath({
                    resourceType,
                    resourceId: resource.id
                  })
                )
              }}
            >
              <div>
                <Text tag='div' weight='semibold'>
                  #{resource.id}
                </Text>
                <Text tag='div' variant='info'>
                  {/* for customers */}
                  {'email' in resource && resource.email}
                  {/* for other markets and skus */}
                  {'name' in resource && resource.name}
                  {/* for other shipments */}
                  {'number' in resource && resource.number}
                </Text>
              </div>
              <Icon name='caretRight' />
            </ListItem>
          )
        }}
      />
    </PageLayout>
  )
}

export default Page

Finally, the details page will use the useCoreApi hook to fetch a single resource. To display the information we’ll use components such as ResourceDetails and ResourceMetadata from App Elements.

import { appRoutes } from '#data/routes'
import {
  EmptyState,
  PageLayout,
  type PageProps,
  ResourceDetails,
  ResourceMetadata,
  Spacer,
  formatResourceName,
  useCoreApi
} from '@commercelayer/app-elements'
import type { FC } from 'react'
import { useLocation } from 'wouter'

const Page: FC<PageProps<typeof appRoutes.details>> = ({ params }) => {
  const { resourceType, resourceId } = params
  const [, setLocation] = useLocation()
  const { data, isLoading, mutate } = useCoreApi(resourceType, 'retrieve', [
    resourceId
  ])

  return (
    <PageLayout
      title={formatResourceName({
        resource: resourceType,
        format: 'title',
        count: 'singular'
      })}
      navigationButton={{
        label: 'Back',
        onClick: () => {
          setLocation(appRoutes.list.makePath({ resourceType }))
        }
      }}
    >
      {isLoading ? (
        <div>Loading...</div>
      ) : data == null ? (
        <EmptyState title='Resource not found' />
      ) : (
        <>
          <Spacer bottom='14'>
            <ResourceDetails
              resource={data}
              onUpdated={async () => {
                void mutate()
              }}
            />
          </Spacer>
          <ResourceMetadata
            resourceType={resourceType}
            resourceId={resourceId}
          />
        </>
      )}
    </PageLayout>
  )
}

export default Page

Step 5

Run it locally

To test your work, start the dev server:

pnpm dev

Go to http://localhost:5173 and you will see a list of all the apps included in this project. You are free to remove any apps from the /apps directory that you do not need, or keep them if you want to modify them later:

Before navigating the apps, you will need an access token in query string, as described in the second step when we introduced the TokenProvider in main.tsx.

Keep in mind that each app requires its own specific access token. Luckily, during development, you can use a single integration token. This token can be generated either via the API or by using the Commerce Layer CLI.

Once you have your development token, simply append it to the URL to open your app:

http://localhost:5173/?accessToken=your_token_here
The custom app homepage
The SKUs listing page
The single SKU details page
Step 6

Deploy and connect to your dashboard

When you are ready, deploy your app to the hosting service of your choice. Then simply add its URL to your Commerce Layer dashboard. From that moment, your entire team can use it as if it was part of the default environment:

Unlocking new possibilities

The custom app we just built is intentionally simple. Yet it shows how easy it is to extend the dashboard and adapt it to your needs.

With App Elements and our starter repo, you have everything you need to create powerful tools that improve your workflows.

Custom apps unlock a new level of flexibility. Whether you are cloning and tweaking an existing app or starting from scratch, you now have the freedom to shape the dashboard around your business.