Introduction

In this tutorial, we will build a B2B React CRM application with Refine Framework and deploy it to an app platform.

At the end of this tutorial, we'll have a CRM application that includes:

  • Dashboard with metric cards and charts.
  • Companies pages to list, create, edit, and show companies.
  • Contacts pages to list, create, edit and show contacts.

While doing these, we'll use the:

  • GraphQL API to fetch the data. Refine has built-in data provider packages for both GraphQL and REST APIs, but you can also build your own to suit your specific requirements. In this guide, we're going to use Nestjs Query as our backend service and the @refinedev/nestjs-query package as our data provider.
  • Ant Design UI library.
  • Once we've built the app, we'll put it online using the cloud provider's App Platform. This service makes it really easy and fast to set up, launch, and grow apps and static websites. You can deploy code by simply pointing to a GitHub repository and let App Platform do the heavy lifting of managing the infrastructure, app runtimes, and dependencies.

You can get the final source code of the application on GitHub.

[slideshow images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png]

Prerequisites

refine illustration for: Prerequisites

Step 1 — What is Refine?

Refine is a React meta-framework for building data-intensive B2B CRUD web applications like internal tools, dashboards, and admin panels. It ships with various hooks and components to reduce the development time and increase the developer experience.

It is designed to build production-ready enterprise B2B apps. Instead of starting from scratch, it provides essential hooks and components to help with tasks such as data & state management, handling authentication, and managing permissions.

So you can focus on building the important parts of your app without getting bogged down in the technical stuff.

Refine is particularly effective in situations where managing data is key, such as:

  • Internal tools:
  • Dashboards:
  • Admin panels:
  • All type of CRUD apps

Customization and styling

Refine's headless architecture allows the flexibility to use any UI library or custom CSS. Additionally, it has built-in support for popular open-source UI libraries, including Ant Design, Material UI, Mantine, and Chakra UI.

Step 2 — Setting Up the Project

We'll use the npm create refine-app command to interactively initialize the project.

				
					npm create refine-app@latest
				
			

Select the following options when prompted:

				
					✔ Choose a project template · Vite
✔ What would you like to name your project?: · crm-app
✔ Choose your backend service to connect: · NestJS Query
✔ Do you want to use a UI Framework?: · Ant Design
✔ Do you need any Authentication logic?: · None
✔ Do you need i18n (Internationalization) support?: · No
✔ Choose a package manager: · npm
				
			

Once the setup is complete, navigate to the project folder and start your app with:

				
					npm run dev
				
			

Open http://localhost:5173 in your browser to see the app.

Step 3 — Building the Dashboard Page

This page will serve as an introduction to the CRM app, showing various metrics and charts. It will be the first page users see when they access the app.

Initially, we'll create a <Dashboard /> component in src/pages/dashboard/index.tsx directory with the following code:

				
					[label src/pages/dashboard/index.tsx]
import { Button } from "antd";
export const Dashboard = () =&gt; {
 return (
 &lt;div&gt;
 &lt;h1&gt;Dashboard&lt;/h1&gt;
 &lt;Button type="primary"&gt;Primary Button&lt;/Button&gt;
 &lt;/div&gt;
 );
};
				
			

To render the component we created in the "/" path, let's add the necessary resources and routes to the <Refine /> component in src/App.tsx.

[details Show <App /> code

				
					[label src/App.tsx]
import { Refine } from '@refinedev/core';
import { RefineKbar, RefineKbarProvider } from '@refinedev/kbar';
import { ThemedLayoutV2, useNotificationProvider } from '@refinedev/antd';
import dataProvider, {
 GraphQLClient,
 liveProvider,
} from '@refinedev/nestjs-query';
import { createClient } from 'graphql-ws';
import { BrowserRouter, Outlet, Route, Routes } from 'react-router-dom';
import routerBindings, {
 UnsavedChangesNotifier,
 DocumentTitleHandler,
} from '@refinedev/react-router-v6';
import {
 DashboardOutlined,
 ShopOutlined,
 TeamOutlined,
} from '@ant-design/icons';
import { ColorModeContextProvider } from './contexts/color-mode';
import { Dashboard } from "./pages/dashboard";
import '@refinedev/antd/dist/reset.css';
const API_URL = 'https://api.crm.refine.dev/graphql';
&lt;^&gt;const WS_URL = 'wss://api.crm.refine.dev/graphql';&lt;^&gt;
const ACCESS_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw';

const gqlClient = new GraphQLClient(API_URL, {
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
});
const wsClient = createClient({
 url: WS_URL,
 connectionParams: () =&gt; ({
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
 }),
});
function App() {
 return (
 &lt;BrowserRouter&gt;
 &lt;RefineKbarProvider&gt;
 &lt;ColorModeContextProvider&gt;
 &lt;Refine
 dataProvider={dataProvider(gqlClient)}
 liveProvider={liveProvider(wsClient)}
 notificationProvider={useNotificationProvider}
 routerProvider={routerBindings}
 &lt;^&gt;resources={[&lt;^&gt;
 &lt;^&gt;{&lt;^&gt;
 &lt;^&gt; name: 'dashboard',&lt;^&gt;
 &lt;^&gt;list: '/',&lt;^&gt;
 &lt;^&gt;meta: {&lt;^&gt;
 &lt;^&gt;icon: &lt;DashboardOutlined /&gt;,&lt;^&gt;
 &lt;^&gt;},&lt;^&gt;
 &lt;^&gt;},&lt;^&gt;
 &lt;^&gt;]}&lt;^&gt;
 options={{
 syncWithLocation: true,
 warnWhenUnsavedChanges: true,
 liveMode: 'auto',
 }}
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 &lt;Route path="/"&gt;
 &lt;Route index element={&lt;Dashboard /&gt;} /&gt;
 &lt;/Route&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 &lt;RefineKbar /&gt;
 &lt;UnsavedChangesNotifier /&gt;
 &lt;DocumentTitleHandler /&gt;
 &lt;/Refine&gt;
 &lt;/ColorModeContextProvider&gt;
 &lt;/RefineKbarProvider&gt;
 &lt;/BrowserRouter&gt;
 );
}
export default App;
				
			

]

We've included the dashboard resource in the resources prop of the <Refine /> component. Additionally, we assigned "/" to the list prop of the dashboard resource, which resulted in the creation of a sidebar menu item.

Additionally, we've added the <Dashboard /> component to the "/" path by using the <Route /> component from the react-router-dom package. We've also added the <ThemedLayoutV2 /> component from the @refinedev/antd package to the <Route /> component to wrap the <Dashboard /> component with the layout component.

[info] Info: You can find more information about resources and adding routes in the React Router v6.

Also, we used the fake CRM GraphQL API to fetch the data, so we updated the values of the following constants API_URL and WS_URL to use the CRM GraphQL API endpoints. Also, this API has some authentication rules, but we won't implement any authentication logic. We disabled the authentication rules by passing the Authorization header to the GraphQLClient constructor.

Now, if you navigate to the "/" path, you should see the <Dashboard /> page.

Creating MetricCard component

Let's create a <MetricCard /> component to show the metrics on the dashboard page. We'll use the Ant Design components to build the metric card and Ant Design Charts to build the chart.

First, we'll install the specific Antd chart package.

				
					npm install @ant-design/plots@1.2.5
				
			

Then, create a src/components/metricCard/index.tsx file with the following code:

[details Show <MetricCard /> component

				
					[label src/components/metricCard/index.tsx]
import React, { FC, PropsWithChildren } from "react";
import { Card, Skeleton, Typography } from "antd";
import { useList } from "@refinedev/core";
import { Area, AreaConfig } from "@ant-design/plots";
import { AuditOutlined, ShopOutlined, TeamOutlined } from "@ant-design/icons";
type MetricType = "companies" | "contacts" | "deals";
export const MetricCard = ({ variant }: { variant: MetricType }) =&gt; {
 const { data, isLoading, isError, error } = useList({
 resource: variant,
 liveMode: "off",
 meta: {
 fields: ["id"],
 },
 });
 if (isError) {
 console.error("Error fetching dashboard data", error);
 return null;
 }
 const { primaryColor, secondaryColor, icon, title } = variants[variant];
 const config: AreaConfig = {
 style: {
 height: "48px",
 width: "100%",
 },
 appendPadding: [1, 0, 0, 0],
 padding: 0,
 syncViewPadding: true,
 data: variants[variant].data,
 autoFit: true,
 tooltip: false,
 animation: false,
 xField: "index",
 yField: "value",
 xAxis: false,
 yAxis: {
 tickCount: 12,
 label: { style: { fill: "transparent" } },
 grid: { line: { style: { stroke: "transparent" } } },
 },
 smooth: true,
 areaStyle: () =&gt; ({
 fill: `l(270) 0:#fff 0.2:${secondaryColor} 1:${primaryColor}`,
 }),
 line: { color: primaryColor },
 };

 return (
 &lt;Card
 bodyStyle={{
 padding: "8px 8px 8px 12px",
 height: "100%",
 display: "flex",
 alignItems: "center",
 justifyContent: "flex-start",
 }}
 size="small"
 &gt;
 &lt;div
 style={{
 display: "flex",
 flexDirection: "column",
 justifyContent: "space-between",
 }}
 &gt;
 &lt;div
 style={{
 display: "flex",
 alignItems: "center",
 gap: "8px",
 whiteSpace: "nowrap",
 }}
 &gt;
 {icon}
 &lt;Typography.Text
 className="secondary"
 style={{ marginLeft: "8px" }}
 &gt;
 {title}
 &lt;/Typography.Text&gt;
 &lt;/div&gt;

 {isLoading ? (
 &lt;div
 style={{
 display: "flex",
 alignItems: "center",
 height: "60px",
 }}
 &gt;
 &lt;Skeleton.Button
 style={{ marginLeft: "48px", marginTop: "8px" }}
 /&gt;
 &lt;/div&gt;
 ) : (
 &lt;Typography.Text
 strong
 style={{
 fontSize: 38,
 textAlign: "start",
 marginLeft: "48px",
 fontVariantNumeric: "tabular-nums",
 }}
 &gt;
 {data?.total}
 &lt;/Typography.Text&gt;
 )}
 &lt;/div&gt;
 &lt;div
 style={{
 marginTop: "auto",
 marginLeft: "auto",
 width: "110px",
 }}
 &gt;
 &lt;Area {...config} /&gt;
 &lt;/div&gt;
 &lt;/Card&gt;
 );
};
const IconWrapper: FC&lt;PropsWithChildren&lt;{ color: string }&gt;&gt; = ({
 color,
 children,
}) =&gt; {
 return (
 &lt;div
 style={{
 display: "flex",
 alignItems: "center",
 justifyContent: "center",
 width: "32px",
 height: "32px",
 borderRadius: "50%",
 backgroundColor: color,
 }}
 &gt;
 {children}
 &lt;/div&gt;
 );
};
const variants: {
 [key in MetricType]: {
 primaryColor: string;
 secondaryColor?: string;
 icon: React.ReactNode;
 title: string;
 data: { index: string; value: number }[];
 };
} = {
 companies: {
 primaryColor: "#1677FF",
 secondaryColor: "#BAE0FF",
 icon: (
 &lt;IconWrapper color="#E6F4FF"&gt;
 &lt;ShopOutlined
 className="md"
 style={{
 color: "#1677FF",
 }}
 /&gt;
 &lt;/IconWrapper&gt;
 ),
 title: "Number of companies",
 data: [
 { index: "1", value: 3500 },
 { index: "2", value: 2750 },
 { index: "3", value: 5000 },
 { index: "4", value: 4250 },
 { index: "5", value: 5000 },
 ],
 },
 contacts: {
 primaryColor: "#52C41A",
 secondaryColor: "#D9F7BE",
 icon: (
 &lt;IconWrapper color="#F6FFED"&gt;
 &lt;TeamOutlined
 className="md"
 style={{
 color: "#52C41A",
 }}
 /&gt;
 &lt;/IconWrapper&gt;
 ),
 title: "Number of contacts",
 data: [
 { index: "1", value: 10000 },
 { index: "2", value: 19500 },
 { index: "3", value: 13000 },
 { index: "4", value: 17000 },
 { index: "5", value: 13000 },
 { index: "6", value: 20000 },
 ],
 },
 deals: {
 primaryColor: "#FA541C",
 secondaryColor: "#FFD8BF",
 icon: (
 &lt;IconWrapper color="#FFF2E8"&gt;
 &lt;AuditOutlined
 className="md"
 style={{
 color: "#FA541C",
 }}
 /&gt;
 &lt;/IconWrapper&gt;
 ),
 title: "Total deals in pipeline",
 data: [
 { index: "1", value: 1000 },
 { index: "2", value: 1300 },
 { index: "3", value: 1200 },
 { index: "4", value: 2000 },
 { index: "5", value: 800 },
 { index: "6", value: 1700 },
 { index: "7", value: 1400 },
 { index: "8", value: 1800 },
 ],
 },
};
				
			

]

In the above code, the component fetches the data from the API and renders the metric card with the data and chart.

For fetching the data, we used the useCustom hook, and we passed the raw query using the meta.rawQuery property. When we pass the raw query by meta.rawQuery prop, @refinedev/nestjs-graphql data provider will pass it to the GraphQL API as it is. This is useful when you want to use some advanced features of the GraphQL API.

We also used the <Area /> component from the @ant-design/plots package to render the chart. We passed the config object to the <Area /> component to configure the chart.

Now, let's update the <Dashboard /> component to use the <MetricCard /> component we created.

				
					[label src/pages/dashboard/index.tsx]
import { Row, Col } from "antd";
import { MetricCard } from "../../components/metricCard";
export const Dashboard = () =&gt; {
 return (
 &lt;Row gutter={[32, 32]}&gt;
 &lt;Col xs={24} sm={24} xl={8}&gt;
 &lt;MetricCard variant="companies" /&gt;
 &lt;/Col&gt;
 &lt;Col xs={24} sm={24} xl={8}&gt;
 &lt;MetricCard variant="contacts" /&gt;
 &lt;/Col&gt;
 &lt;Col xs={24} sm={24} xl={8}&gt;
 &lt;MetricCard variant="deals" /&gt;
 &lt;/Col&gt;
 &lt;/Row&gt;
 );
};
				
			

If you navigate to the "/" path, you should see the updated dashboard page.

Creating DealChart component

We'll add charts to the dashboard to show the deals summary by creating a <DealChart /> component. We'll use the Ant Design Charts to build the chart and Ant Design components to build the card.

First, install the dayjs package for managing the dates.

				
					npm install dayjs
				
			

Create a src/components/dealChart/index.tsx file with the following code:

[details Show <DealChart /> component

				
					[label src/components/dealChart/index.tsx]
import React, { useMemo } from "react";
import { useList } from "@refinedev/core";
import { Card, Typography } from "antd";
import { Area, AreaConfig } from "@ant-design/plots";
import { DollarOutlined } from "@ant-design/icons";
import dayjs from "dayjs";

export const DealChart: React.FC&lt;{}&gt; = () =&gt; {
 const { data } = useList({
 resource: "dealStages",
 filters: [{ field: "title", operator: "in", value: ["WON", "LOST"] }],
 meta: {
 fields: [
 "title",
 {
 dealsAggregate: [
 { groupBy: ["closeDateMonth", "closeDateYear"] },
 { sum: ["value"] },
 ],
 },
 ],
 },
 });

 const dealData = useMemo(() =&gt; {
 const won = data?.data
 .find((node) =&gt; node.title === "WON")
 ?.dealsAggregate.map((item: any) =&gt; {
 const { closeDateMonth, closeDateYear } = item.groupBy!;
 const date = dayjs(`${closeDateYear}-${closeDateMonth}-01`);
 return {
 timeUnix: date.unix(),
 timeText: date.format("MMM YYYY"),
 value: item.sum?.value,
 state: "Won",
 };
 });

 const lost = data?.data
 .find((node) =&gt; node.title === "LOST")
 ?.dealsAggregate.map((item: any) =&gt; {
 const { closeDateMonth, closeDateYear } = item.groupBy!;
 const date = dayjs(`${closeDateYear}-${closeDateMonth}-01`);
 return {
 timeUnix: date.unix(),
 timeText: date.format("MMM YYYY"),
 value: item.sum?.value,
 state: "Lost",
 };
 });

 return [...(won || []), ...(lost || [])].sort(
 (a, b) =&gt; a.timeUnix - b.timeUnix,
 );
 }, [data]);

 const config: AreaConfig = {
 isStack: false,
 data: dealData,
 xField: "timeText",
 yField: "value",
 seriesField: "state",
 animation: true,
 startOnZero: false,
 smooth: true,
 legend: { offsetY: -6 },
 yAxis: {
 tickCount: 4,
 label: {
 formatter: (v) =&gt; `$${Number(v) / 1000}k`,
 },
 },
 tooltip: {
 formatter: (data) =&gt; ({
 name: data.state,
 value: `$${Number(data.value) / 1000}k`,
 }),
 },
 areaStyle: (datum) =&gt; {
 const won = "l(270) 0:#ffffff 0.5:#b7eb8f 1:#52c41a";
 const lost = "l(270) 0:#ffffff 0.5:#f3b7c2 1:#ff4d4f";
 return { fill: datum.state === "Won" ? won : lost };
 },
 color: (datum) =&gt; (datum.state === "Won" ? "#52C41A" : "#F5222D"),
 };

 return (
 &lt;Card
 style={{ height: "432px" }}
 headStyle={{ padding: "8px 16px" }}
 bodyStyle={{ padding: "24px 24px 0px 24px" }}
 title={
 &lt;div
 style={{
 display: "flex",
 alignItems: "center",
 gap: "8px",
 }}
 &gt;
 &lt;DollarOutlined /&gt;
 &lt;Typography.Text style={{ marginLeft: ".5rem" }}&gt;
 Deals
 &lt;/Typography.Text&gt;
 &lt;/div&gt;
 }
 &gt;
 &lt;Area {...config} height={325} /&gt;
 &lt;/Card&gt;
 );
};
				
			

]

In the above code, similar to the <MetricCard /> component, we used the useCustom hook to fetch the data from the GraphQL API and render the chart with the data.

After fetching the data, we grouped the data by the date fields and "LOST" and "WON" deal stages. Then, we passed the grouped data to the data property of the <Area /> component.

Now, let's update the <Dashboard /> component to use the <DealChart /> component we created.

				
					[label src/pages/dashboard/index.tsx]
import { Row, Col } from "antd";
import { MetricCard } from "../../components/metricCard";
import { DealChart } from "../../components/dealChart";

export const Dashboard = () =&gt; {
 return (
 &lt;Row gutter={[32, 32]}&gt;
 &lt;Col xs={24} sm={24} xl={8}&gt;
 &lt;MetricCard variant="companies" /&gt;
 &lt;/Col&gt;
 &lt;Col xs={24} sm={24} xl={8}&gt;
 &lt;MetricCard variant="contacts" /&gt;
 &lt;/Col&gt;
 &lt;Col xs={24} sm={24} xl={8}&gt;
 &lt;MetricCard variant="deals" /&gt;
 &lt;/Col&gt;
 &lt;^&gt;&lt;Col span={24}&gt;&lt;^&gt;
 &lt;^&gt;&lt;DealChart /&gt;&lt;^&gt;
 &lt;^&gt;&lt;/Col&gt;&lt;^&gt;
 &lt;/Row&gt;
 );
};
				
			

The charts will look like below.

Step 4 — Building Company CRUD pages

In this phase, we're going to develop the 'list,' 'create,' 'edit,' and 'show' pages for companies. However, before we start working on these pages, we should first update the <Refine /> component to include the 'companies' resource.

[details Show <App /> code

				
					[label src/App.tsx]
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, useNotificationProvider } from "@refinedev/antd";
import dataProvider, {
 GraphQLClient,
 liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
 UnsavedChangesNotifier,
 DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { DashboardOutlined, ShopOutlined} from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";

import "@refinedev/antd/dist/reset.css";

const API_URL = "https://api.crm.refine.dev/graphql";
const WS_URL = "wss://api.crm.refine.dev/graphql";
const ACCESS_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw";

const gqlClient = new GraphQLClient(API_URL, {
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
});

const wsClient = createClient({
 url: WS_URL,
 connectionParams: () =&gt; ({
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
 }),
});

function App() {
 return (
 &lt;BrowserRouter&gt;
 &lt;RefineKbarProvider&gt;
 &lt;ColorModeContextProvider&gt;
 &lt;Refine
 dataProvider={dataProvider(gqlClient)}
 liveProvider={liveProvider(wsClient)}
 notificationProvider={useNotificationProvider}
 routerProvider={routerBindings}
 resources={[
 {
 name: "dashboard",
 list: "/",
 meta: {
 icon: &lt;DashboardOutlined /&gt;,
 },
 },
 &lt;^&gt;{&lt;^&gt;
 &lt;^&gt;name: "companies",&lt;^&gt;
 &lt;^&gt;list: "/companies",&lt;^&gt;
 &lt;^&gt;create: "/companies/create",&lt;^&gt;
 &lt;^&gt;edit: "/companies/edit/:id",&lt;^&gt;
 &lt;^&gt;show: "/companies/show/:id",&lt;^&gt;
 &lt;^&gt;meta: {&lt;^&gt;
 &lt;^&gt;canDelete: true,&lt;^&gt;
 &lt;^&gt;icon: &lt;ShopOutlined /&gt;,&lt;^&gt;
 &lt;^&gt;},&lt;^&gt;
 &lt;^&gt;},&lt;^&gt;
 ]}
 options={{
 syncWithLocation: true,
 warnWhenUnsavedChanges: true,
 liveMode: "auto",
 }}
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 &lt;Route path="/"&gt;
 &lt;Route index element={&lt;Dashboard /&gt;} /&gt;
 &lt;/Route&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 &lt;RefineKbar /&gt;
 &lt;UnsavedChangesNotifier /&gt;
 &lt;DocumentTitleHandler /&gt;
 &lt;/Refine&gt;
 &lt;/ColorModeContextProvider&gt;
 &lt;/RefineKbarProvider&gt;
 &lt;/BrowserRouter&gt;
 );
}

export default App;
				
			

]

The resource definition mentioned doesn't actually create any CRUD pages itself. Instead, it establishes the routes that these CRUD pages will follow. These routes are essential for ensuring the proper functionality of various Refine hooks and components.

For instance, the useNavigation hook relies on these routes (list, create, edit, and show) to help users navigate between different pages in your application. Additionally, certain data hooks, like useTable, will automatically use the resource name if you don't explicitly provide the resource prop.

List Page

The List page will display company data in a table. To fetch the data, we'll use the useTable hook from @refinedev/antd package, and to render the table, we'll use the <Table /> component from the antd.

Let's create a src/pages/companies/list.tsx file with the following code:

[details Show <CompanyList /> component

				
					[label src/pages/companies/list.tsx]
import React from "react";
import { IResourceComponentsProps, BaseRecord } from "@refinedev/core";
import {
 useTable,
 List,
 EditButton,
 ShowButton,
 DeleteButton,
 UrlField,
 TextField,
} from "@refinedev/antd";
import { Table, Space, Avatar, Input, Form } from "antd";

export const CompanyList: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { tableProps, searchFormProps } = useTable({
 meta: {
 fields: [
 "id",
 "avatarUrl",
 "name",
 "businessType",
 "companySize",
 "country",
 "website",
 { salesOwner: ["id", "name"] },
 ],
 },
 onSearch: (params: { name: string }) =&gt; [
 {
 field: "name",
 operator: "contains",
 value: params.name,
 },
 ],
 });

 return (
 &lt;List
 headerButtons={({ defaultButtons }) =&gt; (
 &lt;&gt;
 &lt;Form
 {...searchFormProps}
 onValuesChange={() =&gt; {
 searchFormProps.form?.submit();
 }}
 &gt;
 &lt;Form.Item noStyle name="name"&gt;
 &lt;Input.Search placeholder="Search by name" /&gt;
 &lt;/Form.Item&gt;
 &lt;/Form&gt;
 {defaultButtons}
 &lt;/&gt;
 )}
 &gt;
 &lt;Table {...tableProps} rowKey="id"&gt;
 &lt;Table.Column
 title="Name"
 render={(
 _,
 record: { name: string; avatarUrl: string },
 ) =&gt; (
 &lt;Space&gt;
 &lt;Avatar
 src={record.avatarUrl}
 size="large"
 shape="square"
 alt={record.name}
 /&gt;
 &lt;TextField value={record.name} /&gt;
 &lt;/Space&gt;
 )}
 /&gt;
 &lt;Table.Column dataIndex="businessType" title="Type" /&gt;
 &lt;Table.Column dataIndex="companySize" title="Size" /&gt;
 &lt;Table.Column dataIndex="country" title="Country" /&gt;
 &lt;Table.Column
 dataIndex={["website"]}
 title="Website"
 render={(value: string) =&gt; &lt;UrlField value={value} /&gt;}
 /&gt;
 &lt;Table.Column
 dataIndex={["salesOwner", "name"]}
 title="Sales Owner"
 /&gt;
 &lt;Table.Column
 title="Actions"
 dataIndex="actions"
 render={(_, record: BaseRecord) =&gt; (
 &lt;Space&gt;
 &lt;EditButton
 hideText
 size="small"
 recordItemId={record.id}
 /&gt;
 &lt;ShowButton
 hideText
 size="small"
 recordItemId={record.id}
 /&gt;
 &lt;DeleteButton
 hideText
 size="small"
 recordItemId={record.id}
 /&gt;
 &lt;/Space&gt;
 )}
 /&gt;
 &lt;/Table&gt;
 &lt;/List&gt;
 );
};
				
			

]

We fetched data using the useTable hook and specified the fields to retrieve by setting them in the meta.fields property. This data was then displayed in a table format using the <Table /> component.

For a better understanding of how GraphQL queries are formulated, you can refer to the GraphQL guide in the documentation.

Our table includes various columns like company name, business type, size, country, website, and sales owner. We followed the guidelines from the Ant Design Table for setting up these columns and used specific components like <TextField /> and <UrlField /> from @refinedev/antd and <Avatar /> from antd for customization.

We added functionality with <EditButton />, <ShowButton />, and <DeleteButton /> components for different actions on the records.

For filtering, we utilized the useTable features, implementing a search form with the <Form /> component. User searches trigger through the onSearch prop of useTable.

To compile the company CRUD pages, we need to create an index.ts file in the src/pages/companies directory, following the provided code.

				
					export * from "./list";
				
			

Next, import the <CompanyList /> component in src/App.tsx and add a route for rendering it.

[details Show <App /> code

				
					[label src/App.tsx]
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, useNotificationProvider } from "@refinedev/antd";
import dataProvider, {
 GraphQLClient,
 liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
 UnsavedChangesNotifier,
 DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import { DashboardOutlined, ShopOutlined } from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import { CompanyList } from "./pages/companies";

import "@refinedev/antd/dist/reset.css";

const API_URL = 'https://api.crm.refine.dev/graphql';
const WS_URL = 'wss://api.crm.refine.dev/graphql';
const ACCESS_TOKEN =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw';

const gqlClient = new GraphQLClient(API_URL, {
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
});

const wsClient = createClient({
 url: WS_URL,
 connectionParams: () =&gt; ({
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
 }),
});

function App() {
 return (
 &lt;BrowserRouter&gt;
 &lt;RefineKbarProvider&gt;
 &lt;ColorModeContextProvider&gt;
 &lt;Refine
 dataProvider={dataProvider(gqlClient)}
 liveProvider={liveProvider(wsClient)}
 notificationProvider={useNotificationProvider}
 routerProvider={routerBindings}
 resources={[
 {
 name: "dashboard",
 list: "/",
 meta: {
 icon: &lt;DashboardOutlined /&gt;,
 },
 },
 {
 name: "companies",
 list: "/companies",
 create: "/companies/create",
 edit: "/companies/edit/:id",
 show: "/companies/show/:id",
 meta: {
 canDelete: true,
 icon: &lt;ShopOutlined /&gt;,
 },
 },
 ]}
 options={{
 syncWithLocation: true,
 warnWhenUnsavedChanges: true,
 liveMode: "auto",
 }}
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 &lt;Route path="/"&gt;
 &lt;Route index element={&lt;Dashboard /&gt;} /&gt;
 &lt;/Route&gt;
 &lt;^&gt;&lt;Route path="/companies"&gt;&lt;^&gt;
 &lt;^&gt;&lt;Route index element={&lt;CompanyList /&gt;} /&gt;&lt;^&gt;
 &lt;^&gt;&lt;/Route&gt;&lt;^&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 &lt;RefineKbar /&gt;
 &lt;UnsavedChangesNotifier /&gt;
 &lt;DocumentTitleHandler /&gt;
 &lt;/Refine&gt;
 &lt;/ColorModeContextProvider&gt;
 &lt;/RefineKbarProvider&gt;
 &lt;/BrowserRouter&gt;
 );
}

export default App;
				
			

]

Now, if you navigate to the "/companies" path, you should see the list page.

Create Page

The Create page will show a form to create a new company record.

We will use the <Form /> component, and for managing form submissions, the useForm hook will be utilized.

Let's create a src/pages/companies/create.tsx file with the following code:

[details Show <CompanyCreate /> component

				
					[label src/pages/companies/create.tsx]
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const CompanyCreate: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { formProps, saveButtonProps } = useForm();

 const { selectProps } = useSelect({
 resource: "users",
 meta: {
 fields: ["name", "id"],
 },
 optionLabel: "name",
 });

 return (
 &lt;Create saveButtonProps={saveButtonProps}&gt;
 &lt;Form {...formProps} layout="vertical"&gt;
 &lt;Form.Item
 label="Name"
 name={["name"]}
 rules={[{ required: true }]}
 &gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item
 label="Sales Owner"
 name="salesOwnerId"
 rules={[{ required: true }]}
 &gt;
 &lt;Select {...selectProps} /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Business Type" name={["businessType"]}&gt;
 &lt;Select
 options={[
 { label: "B2B", value: "B2B" },
 { label: "B2C", value: "B2C" },
 { label: "B2G", value: "B2G" },
 ]}
 /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Company Size" name={["companySize"]}&gt;
 &lt;Select
 options={[
 { label: "Enterprise", value: "ENTERPRISE" },
 { label: "Large", value: "LARGE" },
 { label: "Medium", value: "MEDIUM" },
 { label: "Small", value: "SMALL" },
 ]}
 /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Country" name={["country"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Website" name={["website"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;/Form&gt;
 &lt;/Create&gt;
 );
};
				
			

]

We used the useSelect hook to fetch relationship data. We passed the selectProps to the <Select /> component to render the options.

You might have noticed that we didn't use the meta.fields prop in the useForm hook. This is because we don't need to retrieve data of the record that was just created after submitting the form. However, if you require this data, you can include the meta.fields prop.

To export the component, let's update the src/pages/company/index.ts file with the following code:

				
					export * from "./list";
export * from "./create";
				
			

Next, import the <CompanyCreate /> component in src/App.tsx and add a route for rendering it.

[details Show <App /> code

The highlighted code shows the new code. You can copy and paste the highlighted code to your src/App.tsx file.

				
					[label src/App.tsx]
//...
import { CompanyList, CompanyCreate } from "./pages/companies";
function App() {
 return (
 //...
 &lt;Refine
 //...
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 //...
 &lt;Route path="/companies"&gt;
 &lt;Route index element={&lt;CompanyList /&gt;} /&gt;
 &lt;^&gt;&lt;Route path="create" element={&lt;CompanyCreate /&gt;} /&gt;&lt;^&gt;
 &lt;/Route&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 // ...
 &lt;/Refine&gt;
 //...
 );
}
export default App;
				
			

]

Now, if you navigate to the "/companies/create" path, you should see the create page.

Edit Page

The edit page will show a form to edit an existing company record. The form will be the same as the create page. However, it will be filled with the existing record data.

Let's create a src/pages/companies/edit.tsx file with the following code:

[details Show <CompanyEdit /> component

				
					[label src/pages/companies/edit.tsx]
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const CompanyEdit: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { formProps, saveButtonProps } = useForm({
 meta: {
 fields: [
 "id",
 "name",
 "businessType",
 "companySize",
 "country",
 "website",
 ],
 },
 });

 return (
 &lt;Edit saveButtonProps={saveButtonProps}&gt;
 &lt;Form {...formProps} layout="vertical"&gt;
 &lt;Form.Item
 label="Name"
 name={["name"]}
 rules={[{ required: true }]}
 &gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Business Type" name={["businessType"]}&gt;
 &lt;Select
 options={[
 { label: "B2B", value: "B2B" },
 { label: "B2C", value: "B2C" },
 { label: "B2G", value: "B2G" },
 ]}
 /&gt;
 &lt;/Form.Item&gt;

 &lt;Form.Item label="Company Size" name={["companySize"]}&gt;
 &lt;Select
 options={[
 { label: "Enterprise", value: "ENTERPRISE" },
 { label: "Large", value: "LARGE" },
 { label: "Medium", value: "MEDIUM" },
 { label: "Small", value: "SMALL" },
 ]}
 /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Country" name={["country"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Website" name={["website"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;/Form&gt;
 &lt;/Edit&gt;
 );
};
				
			

]

We used the useForm hook to handle the form submission. We passed the formProps and saveButtonProps to the <Form /> component and <Edit /> component respectively.

We specified the fields we wanted to fill the form with by passing them to the meta.fields property.

To render the form fields, we used input components from the antd library.

To export the component, let's update the src/pages/company/index.ts file with the following code:

				
					export * from "./list";
export * from "./create";
export * from "./edit";
				
			

Next, import the <CompanyEdit /> component in src/App.tsx and add a route for rendering it.

[details Show <App /> code

The highlighted code shows the new code. You can copy and paste the highlighted code to your src/App.tsx file.

				
					[label src/App.tsx]
//...
import { CompanyList, CompanyCreate, CompanyEdit } from "./pages/companies";

function App() {
 return (
 //...
 &lt;Refine
 //...
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 //...

 &lt;Route path="/companies"&gt;
 &lt;Route index element={&lt;CompanyList /&gt;} /&gt;
 &lt;Route path="create" element={&lt;CompanyCreate /&gt;} /&gt;
 &lt;^&gt;&lt;Route path="edit/:id" element={&lt;CompanyEdit /&gt;} /&gt;&lt;^&gt;
 &lt;/Route&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 // ...
 &lt;/Refine&gt;
 );
}

export default App;
				
			

]

Now, if you navigate to the "/companies/:id/edit" path, you should see the edit page.

Show Page

The show page will show the details of an existing company record.

Let's create a src/pages/companies/show.tsx file with the following code:

[details Show <CompanyShow /> component

				
					[label src/pages/companies/show.tsx]
import React from "react";
import { IResourceComponentsProps, useShow } from "@refinedev/core";
import { Show, NumberField, TextField } from "@refinedev/antd";
import { Typography } from "antd";

const { Title } = Typography;

export const CompanyShow: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { queryResult } = useShow({
 meta: {
 fields: [
 "id",
 "name",
 "businessType",
 "companySize",
 "country",
 "website",
 ],
 },
 });
 const { data, isLoading } = queryResult;

 const record = data?.data;

 return (
 &lt;Show isLoading={isLoading}&gt;
 &lt;Title level={5}&gt;Id&lt;/Title&gt;
 &lt;NumberField value={record?.id ?? ""} /&gt;
 &lt;Title level={5}&gt;Name&lt;/Title&gt;
 &lt;TextField value={record?.name} /&gt;
 &lt;Title level={5}&gt;Business Type&lt;/Title&gt;
 &lt;TextField value={record?.businessType} /&gt;
 &lt;Title level={5}&gt;Company Size&lt;/Title&gt;
 &lt;TextField value={record?.companySize} /&gt;
 &lt;Title level={5}&gt;Country&lt;/Title&gt;
 &lt;TextField value={record?.country} /&gt;
 &lt;Title level={5}&gt;Website&lt;/Title&gt;
 &lt;TextField value={record?.website} /&gt;
 &lt;/Show&gt;
 );
};
				
			

]

To fetch the data, we'll use the useShow hook. Again, we specified the fields we wanted to fetch by passing them to the meta.fields property. We then passed the resulting queryResult.isLoading to the <Show /> component to show the loading indicator while fetching the data.

To render the record data, we used the <NumberField /> and <TextField /> components from the @refinedev/antd package.

To export the component, let's update the src/pages/company/index.ts file with the following code:

				
					export * from "./list";
export * from "./create";
export * from "./edit";
&lt;^&gt;export * from "./show";&lt;^&gt;
				
			

Next, import the <CompanyShow /> component in src/App.tsx and add a route for rendering it.

[details Show <App /> code

The highlighted code shows the new code. You can copy and paste the highlighted code to your src/App.tsx file.

				
					[label src/App.tsx]
//...
import {
 CompanyList,
 CompanyCreate,
 CompanyEdit,
 &lt;^&gt;CompanyShow&lt;^&gt;,
} from "./pages/companies";

function App() {
 return (
 //...
 &lt;Refine
 //...
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 //...

 &lt;Route path="/companies"&gt;
 &lt;Route index element={&lt;CompanyList /&gt;} /&gt;
 &lt;Route path="create" element={&lt;CompanyCreate /&gt;} /&gt;
 &lt;Route path="edit/:id" element={&lt;CompanyEdit /&gt;} /&gt;
 &lt;^&gt;&lt;Route path="show/:id" element={&lt;CompanyShow /&gt;} /&gt;&lt;^&gt;
 &lt;/Route&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 // ...
 &lt;/Refine&gt;
 );
}

export default App;
				
			

]

Now, if you navigate to the "/companies/show/:id" path, you should see the show page.

Step 5 — Building Contacts CRUD pages

In the previous step, we built the company CRUD pages. In this step, we'll build the contact CRUD pages similarly. So, we won't repeat the explanations we made in the previous step. If you need more information about the steps, you can refer to the previous step.

Let's start by defining the contact resource in src/App.tsx file as follows:

[details Show <App /> code

				
					[label src/App.tsx]
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, useNotificationProvider } from "@refinedev/antd";
import dataProvider, {
 GraphQLClient,
 liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
 UnsavedChangesNotifier,
 DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import {
 DashboardOutlined,
 ShopOutlined,
 &lt;^&gt;TeamOutlined&lt;^&gt;,
} from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import {
 CompanyList,
 CompanyCreate,
 CompanyEdit,
 CompanyShow,
} from "./pages/companies";

import "@refinedev/antd/dist/reset.css";

const API_URL = 'https://api.crm.refine.dev/graphql';
const WS_URL = 'wss://api.crm.refine.dev/graphql';
const ACCESS_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw';

const gqlClient = new GraphQLClient(API_URL, {
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
});

const wsClient = createClient({
 url: WS_URL,
 connectionParams: () =&gt; ({
 headers: {
 Authorization: `Bearer ${ACCESS_TOKEN}`,
 },
 }),
});


function App() {
 return (
 &lt;BrowserRouter&gt;
 &lt;RefineKbarProvider&gt;
 &lt;ColorModeContextProvider&gt;
 &lt;Refine
 dataProvider={dataProvider(gqlClient)}
 liveProvider={liveProvider(wsClient)}
 notificationProvider={useNotificationProvider}
 routerProvider={routerBindings}
 resources={[
 {
 name: "dashboard",
 list: "/",
 meta: {
 icon: &lt;DashboardOutlined /&gt;,
 },
 },
 {
 name: "companies",
 list: "/companies",
 create: "/companies/create",
 edit: "/companies/edit/:id",
 show: "/companies/show/:id",
 meta: {
 canDelete: true,
 icon: &lt;ShopOutlined /&gt;,
 },
 },
 &lt;^&gt;{&lt;^&gt;
 &lt;^&gt;name: "contacts",&lt;^&gt;
 &lt;^&gt;list: "/contacts",&lt;^&gt;
 &lt;^&gt;create: "/contacts/create",&lt;^&gt;
 &lt;^&gt;edit: "/contacts/edit/:id",&lt;^&gt;
 &lt;^&gt;show: "/contacts/show/:id",&lt;^&gt;
 &lt;^&gt;meta: {&lt;^&gt;
 &lt;^&gt;canDelete: true,&lt;^&gt;
 &lt;^&gt;icon: &lt;TeamOutlined /&gt;,&lt;^&gt;
 &lt;^&gt;},&lt;^&gt;
 &lt;^&gt;},&lt;^&gt;
 ]}
 options={{
 syncWithLocation: true,
 warnWhenUnsavedChanges: true,
 liveMode: "auto",
 }}
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 &lt;Route path="/"&gt;
 &lt;Route index element={&lt;Dashboard /&gt;} /&gt;
 &lt;/Route&gt;

 &lt;Route path="/companies"&gt;
 &lt;Route index element={&lt;CompanyList /&gt;} /&gt;
 &lt;Route
 path="create"
 element={&lt;CompanyCreate /&gt;}
 /&gt;
 &lt;Route
 path="edit/:id"
 element={&lt;CompanyEdit /&gt;}
 /&gt;
 &lt;Route
 path="show/:id"
 element={&lt;CompanyShow /&gt;}
 /&gt;
 &lt;/Route&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 &lt;RefineKbar /&gt;
 &lt;UnsavedChangesNotifier /&gt;
 &lt;DocumentTitleHandler /&gt;
 &lt;/Refine&gt;
 &lt;/ColorModeContextProvider&gt;
 &lt;/RefineKbarProvider&gt;
 &lt;/BrowserRouter&gt;
 );
}

export default App;
				
			

]

Let's create CRUD pages for the contact resource as follows:

Create src/pages/contacts/list.tsx file with the following code:

[details Show <ContactList /> component

				
					[label src/pages/contacts/list.tsx]
import React from "react";
import { IResourceComponentsProps, BaseRecord } from "@refinedev/core";
import {
 useTable,
 List,
 EditButton,
 ShowButton,
 DeleteButton,
 EmailField,
 TextField,
} from "@refinedev/antd";
import { Table, Space, Avatar, Form, Input } from "antd";

export const ContactList: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { tableProps, searchFormProps } = useTable({
 meta: {
 fields: [
 "avatarUrl",
 "id",
 "name",
 "email",
 { company: ["id", "name"] },
 "jobTitle",
 "phone",
 "status",
 ],
 },
 onSearch: (params: { name: string }) =&gt; [
 {
 field: "name",
 operator: "contains",
 value: params.name,
 },
 ],
 });

 return (
 &lt;List
 headerButtons={({ defaultButtons }) =&gt; (
 &lt;&gt;
 &lt;Form
 {...searchFormProps}
 onValuesChange={() =&gt; {
 searchFormProps.form?.submit();
 }}
 &gt;
 &lt;Form.Item noStyle name="name"&gt;
 &lt;Input.Search placeholder="Search by name" /&gt;
 &lt;/Form.Item&gt;
 &lt;/Form&gt;
 {defaultButtons}
 &lt;/&gt;
 )}
 &gt;
 &lt;Table {...tableProps} rowKey="id"&gt;
 &lt;Table.Column
 title="Name"
 width={200}
 render={(
 _,
 record: { name: string; avatarUrl: string },
 ) =&gt; (
 &lt;Space&gt;
 &lt;Avatar src={record.avatarUrl} alt={record.name} /&gt;
 &lt;TextField value={record.name} /&gt;
 &lt;/Space&gt;
 )}
 /&gt;
 &lt;Table.Column dataIndex={["company", "name"]} title="Company" /&gt;
 &lt;Table.Column dataIndex="jobTitle" title="Job Title" /&gt;
 &lt;Table.Column
 dataIndex={["email"]}
 title="Email"
 render={(value) =&gt; &lt;EmailField value={value} /&gt;}
 /&gt;
 &lt;Table.Column dataIndex="phone" title="Phone" /&gt;
 &lt;Table.Column dataIndex="status" title="Status" /&gt;
 &lt;Table.Column
 title="Actions"
 dataIndex="actions"
 render={(_, record: BaseRecord) =&gt; (
 &lt;Space&gt;
 &lt;EditButton
 hideText
 size="small"
 recordItemId={record.id}
 /&gt;
 &lt;ShowButton
 hideText
 size="small"
 recordItemId={record.id}
 /&gt;
 &lt;DeleteButton
 hideText
 size="small"
 recordItemId={record.id}
 /&gt;
 &lt;/Space&gt;
 )}
 /&gt;
 &lt;/Table&gt;
 &lt;/List&gt;
 );
};
				
			

]

Create src/pages/contacts/create.tsx file with the following code:

[details Show <ContactCreate /> component

				
					[label src/pages/contacts/create.tsx]
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Create, useForm, useSelect } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const ContactCreate: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { formProps, saveButtonProps } = useForm();

 const { selectProps: companySelectProps } = useSelect({
 resource: "companies",
 optionLabel: "name",
 meta: {
 fields: ["id", "name"],
 },
 });

 const { selectProps: salesOwnerSelectProps } = useSelect({
 resource: "users",
 meta: {
 fields: ["name", "id"],
 },
 optionLabel: "name",
 });

 return (
 &lt;Create saveButtonProps={saveButtonProps}&gt;
 &lt;Form {...formProps} layout="vertical"&gt;
 &lt;Form.Item
 label="Name"
 name={["name"]}
 rules={[{ required: true }]}
 &gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item
 label="Email"
 name={["email"]}
 rules={[{ required: true }]}
 &gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item
 label="Company"
 name={["companyId"]}
 rules={[{ required: true }]}
 &gt;
 &lt;Select {...companySelectProps} /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item
 label="Sales Owner"
 name="salesOwnerId"
 rules={[{ required: true }]}
 &gt;
 &lt;Select {...salesOwnerSelectProps} /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Job Title" name={["jobTitle"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Phone" name={["phone"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Status" name={["status"]}&gt;
 &lt;Select
 options={[
 { label: "NEW", value: "NEW" },
 { label: "CONTACTED", value: "CONTACTED" },
 { label: "INTERESTED", value: "INTERESTED" },
 { label: "UNQUALIFIED", value: "UNQUALIFIED" },
 { label: "QUALIFIED", value: "QUALIFIED" },
 { label: "NEGOTIATION", value: "NEGOTIATION" },
 { label: "LOST", value: "LOST" },
 { label: "WON", value: "WON" },
 { label: "CHURNED", value: "CHURNED" },
 ]}
 /&gt;
 &lt;/Form.Item&gt;
 &lt;/Form&gt;
 &lt;/Create&gt;
 );
};
				
			

]

Create src/pages/contacts/edit.tsx file with the following code:

[details Show <ContactEdit /> component

				
					[label src/pages/contacts/edit.tsx]
import React from "react";
import { IResourceComponentsProps } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";
import { Form, Input, Select } from "antd";

export const ContactEdit: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { formProps, saveButtonProps } = useForm({
 meta: {
 fields: [
 "avatarUrl",
 "id",
 "name",
 "email",
 "jobTitle",
 "phone",
 "status",
 ],
 },
 });

 return (
 &lt;Edit saveButtonProps={saveButtonProps}&gt;
 &lt;Form {...formProps} layout="vertical"&gt;
 &lt;Form.Item
 label="Name"
 name={["name"]}
 rules={[{ required: true }]}
 &gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item
 label="Email"
 name={["email"]}
 rules={[{ required: true }]}
 &gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Job Title" name={["jobTitle"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Phone" name={["phone"]}&gt;
 &lt;Input /&gt;
 &lt;/Form.Item&gt;
 &lt;Form.Item label="Status" name={["status"]}&gt;
 &lt;Select
 options={[
 { label: "NEW", value: "NEW" },
 { label: "CONTACTED", value: "CONTACTED" },
 { label: "INTERESTED", value: "INTERESTED" },
 { label: "UNQUALIFIED", value: "UNQUALIFIED" },
 { label: "QUALIFIED", value: "QUALIFIED" },
 { label: "NEGOTIATION", value: "NEGOTIATION" },
 { label: "LOST", value: "LOST" },
 { label: "WON", value: "WON" },
 { label: "CHURNED", value: "CHURNED" },
 ]}
 /&gt;
 &lt;/Form.Item&gt;
 &lt;/Form&gt;
 &lt;/Edit&gt;
 );
};
				
			

]

Create src/pages/contacts/show.tsx file with the following code:

[details Show <ContactShow /> component

				
					[label src/pages/contacts/show.tsx]
import React from "react";
import { IResourceComponentsProps, useShow } from "@refinedev/core";
import { Show, NumberField, TextField, EmailField } from "@refinedev/antd";
import { Typography } from "antd";

const { Title } = Typography;

export const ContactShow: React.FC&lt;IResourceComponentsProps&gt; = () =&gt; {
 const { queryResult } = useShow({
 meta: {
 fields: [
 "id",
 "name",
 "email",
 { company: ["id", "name"] },
 "jobTitle",
 "phone",
 "status",
 ],
 },
 });
 const { data, isLoading } = queryResult;
 const record = data?.data;

 return (
 &lt;Show isLoading={isLoading}&gt;
 &lt;Title level={5}&gt;Id&lt;/Title&gt;
 &lt;NumberField value={record?.id ?? ""} /&gt;
 &lt;Title level={5}&gt;Name&lt;/Title&gt;
 &lt;TextField value={record?.name} /&gt;
 &lt;Title level={5}&gt;Email&lt;/Title&gt;
 &lt;EmailField value={record?.email} /&gt;
 &lt;Title level={5}&gt;Company&lt;/Title&gt;
 &lt;TextField value={record?.company?.name} /&gt;
 &lt;Title level={5}&gt;Job Title&lt;/Title&gt;
 &lt;TextField value={record?.jobTitle} /&gt;
 &lt;Title level={5}&gt;Phone&lt;/Title&gt;
 &lt;TextField value={record?.phone} /&gt;
 &lt;Title level={5}&gt;Status&lt;/Title&gt;
 &lt;TextField value={record?.status} /&gt;
 &lt;/Show&gt;
 );
};
				
			

]

After creating the CRUD pages, let's create a src/pages/contacts/index.ts file to export the pages as follows:

				
					export * from "./list";
export * from "./create";
export * from "./edit";
export * from "./show";
				
			

To render the contact CRUD pages, let's update the src/App.tsx file with the following code:

[details Show <App /> code

				
					[label src/App.tsx]
import { Refine } from "@refinedev/core";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import { ThemedLayoutV2, notificationProvider } from "@refinedev/antd";
import dataProvider, {
 GraphQLClient,
 liveProvider,
} from "@refinedev/nestjs-query";
import { createClient } from "graphql-ws";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import routerBindings, {
 UnsavedChangesNotifier,
 DocumentTitleHandler,
} from "@refinedev/react-router-v6";
import {
 DashboardOutlined,
 ShopOutlined,
 TeamOutlined,
} from "@ant-design/icons";

import { ColorModeContextProvider } from "./contexts/color-mode";
import { Dashboard } from "./pages/dashboard";
import {
 CompanyList,
 CompanyCreate,
 CompanyEdit,
 CompanyShow,
} from "./pages/companies";
import {
 &lt;^&gt;ContactList,&lt;^&gt;
 &lt;^&gt;ContactCreate,&lt;^&gt;
 &lt;^&gt;ContactEdit,&lt;^&gt;
 &lt;^&gt;ContactShow,&lt;^&gt;
} from "./pages/contacts";

import "@refinedev/antd/dist/reset.css";

const API_URL = "https://api.crm.refine.dev/graphql";
const WS_URL = "wss://api.crm.refine.dev/graphql";

const gqlClient = new GraphQLClient(API_URL, {
 headers: {
 Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiamltLmhhbHBlcnRAZHVuZGVybWlmZmxpbi5jb20iLCJpYXQiOjE2OTQ2ODI0OTksImV4cCI6MTg1MjQ3MDQ5OX0.4PF7-VYY4tlpuvGHmsunaH_ETLd-N_ANSjEB_NiPExw`,
 },
});
const wsClient = createClient({ url: WS_URL });

function App() {
 return (
 &lt;BrowserRouter&gt;
 &lt;RefineKbarProvider&gt;
 &lt;ColorModeContextProvider&gt;
 &lt;Refine
 dataProvider={dataProvider(gqlClient)}
 liveProvider={liveProvider(wsClient)}
 notificationProvider={notificationProvider}
 routerProvider={routerBindings}
 resources={[
 {
 name: "dashboard",
 list: "/",
 meta: {
 icon: &lt;DashboardOutlined /&gt;,
 },
 },
 {
 name: "companies",
 list: "/companies",
 create: "/companies/create",
 edit: "/companies/edit/:id",
 show: "/companies/show/:id",
 meta: {
 canDelete: true,
 icon: &lt;ShopOutlined /&gt;,
 },
 },
 {
 name: "contacts",
 list: "/contacts",
 create: "/contacts/create",
 edit: "/contacts/edit/:id",
 show: "/contacts/show/:id",
 meta: {
 canDelete: true,
 icon: &lt;TeamOutlined /&gt;,
 },
 },
 ]}
 options={{
 syncWithLocation: true,
 warnWhenUnsavedChanges: true,
 liveMode: "auto",
 }}
 &gt;
 &lt;Routes&gt;
 &lt;Route
 element={
 &lt;ThemedLayoutV2&gt;
 &lt;Outlet /&gt;
 &lt;/ThemedLayoutV2&gt;
 }
 &gt;
 &lt;Route path="/"&gt;
 &lt;Route index element={&lt;Dashboard /&gt;} /&gt;
 &lt;/Route&gt;
 &lt;Route path="/companies"&gt;
 &lt;Route index element={&lt;CompanyList /&gt;} /&gt;
 &lt;Route
 path="create"
 element={&lt;CompanyCreate /&gt;}
 /&gt;
 &lt;Route
 path="edit/:id"
 element={&lt;CompanyEdit /&gt;}
 /&gt;
 &lt;Route
 path="show/:id"
 element={&lt;CompanyShow /&gt;}
 /&gt;
 &lt;/Route&gt;
 &lt;^&gt;&lt;Route path="/contacts"&gt;&lt;^&gt;
 &lt;^&gt;&lt;Route index element={&lt;ContactList /&gt;} /&gt;&lt;^&gt;
 &lt;^&gt;&lt;Route&lt;^&gt;
 &lt;^&gt;path="create"&lt;^&gt;
 &lt;^&gt;element={&lt;ContactCreate /&gt;}&lt;^&gt;
 &lt;^&gt;/&gt;&lt;^&gt;
 &lt;^&gt;&lt;Route&lt;^&gt;
 &lt;^&gt;path="edit/:id"&lt;^&gt;
 &lt;^&gt;element={&lt;ContactEdit /&gt;}&lt;^&gt;
 &lt;^&gt;/&gt;&lt;^&gt;
 &lt;^&gt;&lt;Route&lt;^&gt;
 &lt;^&gt;path="show/:id"&lt;^&gt;
 &lt;^&gt;element={&lt;ContactShow /&gt;}&lt;^&gt;
 &lt;^&gt;/&gt;&lt;^&gt;
 &lt;^&gt;&lt;/Route&gt;&lt;^&gt;
 &lt;/Route&gt;
 &lt;/Routes&gt;
 &lt;RefineKbar /&gt;
 &lt;UnsavedChangesNotifier /&gt;
 &lt;DocumentTitleHandler /&gt;
 &lt;/Refine&gt;
 &lt;/ColorModeContextProvider&gt;
 &lt;/RefineKbarProvider&gt;
 &lt;/BrowserRouter&gt;
 );
}
export default App;
				
			

]

After these changes, you should be able to navigate to the contact CRUD pages as below:

[slideshow images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png images/building-crm-with-refine-and-cloud-provider-section-1.png]

Step 6 — Deploying to the an app platform

In this step, we'll deploy the application to the an app platform. To do that, we'll host the source code on GitHub and connect the GitHub repository to the App Platform.

Pushing the Code to GitHub

Log in to your GitHub account and create a new repository named crm-app. You can make the repository public or private:

After creating the repository, navigate to the project directory and run the following command to initialize a new Git repository:

				
					git init
				
			

Next, add all the files to the Git repository with this command:

				
					git add .
				
			

Then, commit the files with this command:

				
					git commit -m "Initial commit"
				
			

Next, add the GitHub repository as a remote repository with this command:

				
					git remote add origin &lt;your-github-repository-url&gt;
				
			

Next, specify that you want to push your code to the main branch with this command:

				
					git branch -M main
				
			

Finally, push the code to the GitHub repository with this command:

				
					git push -u origin main
				
			

When prompted, enter your GitHub credentials to push your code.

You'll receive a success message after the code is pushed to the GitHub repository.

In this section, you pushed your project to GitHub so that you can access it using the cloud provider Apps. The next step is to create a new the cloud provider App using your project and set up automatic deployment.

Deploying to the an app platform

In this step, you'll take your React application and set it up on the an app platform. You'll link your GitHub repository to the cloud provider, set up the building process, and create your initial project deployment. Once your project is live, any future changes you make will automatically trigger a new build and update.

By the end of this step, you'll have successfully deployed your application on the cloud provider with continuous delivery capabilities.

Log in to your cloud account and navigate to the Apps page. Click the Create App button:

If you haven't connected your GitHub account to the cloud provider, you'll be prompted to do so. Click the Connect to GitHub button. A new window will open, asking you to authorize the cloud provider to access your GitHub account.

After you authorize the cloud provider, you'll be redirected back to the the cloud provider Apps page. The next step is to select your GitHub repository. After you select your repository, you'll be prompted to select a branch to deploy. Select the main branch and click the Next button.

After that, you'll see the configuration steps for your application. In this tutorial, you can click the Next button to skip the configuration steps. However, you can also configure your application as you wish.

Wait for the build to complete. After the build is complete, press Live App to access your project in the browser. It will be the same as the project you tested locally, but this will be live on the web with a secure URL. Also, you can follow this tutorial available on the the cloud provider community site to learn how to deploy react-based applications to App Platform.

Conclusion

In this tutorial, we built a React CRM application using Refine from scratch and got familiar with how to build a fully functional CRUD app.

Also, we'll demonstrate how to deploy your application to the an app platform.

If you want to learn more about Refine, you can check out the documentation, and if you have any questions or feedback, you can join the Refine Discord Server.