diff --git a/frontend/src/metabase-types/api/dashboard.ts b/frontend/src/metabase-types/api/dashboard.ts
index ccdd984e8807244b3c50632f553f58ea75713ba3..55e17e7d780db0ecedfeb3bb7404fcd3987fe6c0 100644
--- a/frontend/src/metabase-types/api/dashboard.ts
+++ b/frontend/src/metabase-types/api/dashboard.ts
@@ -39,7 +39,9 @@ export interface Dashboard {
   model?: string;
   dashcards: DashboardCard[];
   tabs?: DashboardTab[];
+  show_in_getting_started?: boolean | null;
   parameters?: Parameter[] | null;
+  point_of_interest?: string | null;
   collection_authority_level?: CollectionAuthorityLevel;
   can_write: boolean;
   cache_ttl: number | null;
@@ -201,3 +203,52 @@ export interface GetCompatibleCardsPayload {
   query?: string;
   exclude_ids: number[];
 }
+
+export type ListDashboardsRequest = {
+  f?: "all" | "mine" | "archived";
+};
+
+export type GetDashboardRequest = {
+  id: DashboardId;
+  ignore_error?: boolean;
+};
+
+export type CreateDashboardRequest = {
+  name: string;
+  description?: string | null;
+  parameters?: Parameter[] | null;
+  cache_ttl?: number;
+  collection_id?: CollectionId | null;
+  collection_position?: number | null;
+};
+
+export type UpdateDashboardRequest = {
+  id: DashboardId;
+  parameters?: Parameter[] | null;
+  point_of_interest?: string | null;
+  description?: string | null;
+  archived?: boolean | null;
+  dashcards?: DashboardCard[] | null;
+  collection_position?: number | null;
+  tabs?: DashboardTab[];
+  show_in_getting_started?: boolean | null;
+  enable_embedding?: boolean | null;
+  collection_id?: CollectionId | null;
+  name?: string | null;
+  width?: DashboardWidth | null;
+  caveats?: string | null;
+  embedding_params?: EmbeddingParameters | null;
+  cache_ttl?: number;
+  position?: number | null;
+};
+
+export type SaveDashboardRequest = Omit<UpdateDashboardRequest, "id">;
+
+export type CopyDashboardRequest = {
+  id: DashboardId;
+  name?: string | null;
+  description?: string | null;
+  collection_id?: CollectionId | null;
+  collection_position?: number | null;
+  is_deep_copy?: boolean | null;
+};
diff --git a/frontend/src/metabase/api/dashboard.ts b/frontend/src/metabase/api/dashboard.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1d2bf1f34f21c93e3ce8ca745868ec7732eba22f
--- /dev/null
+++ b/frontend/src/metabase/api/dashboard.ts
@@ -0,0 +1,96 @@
+import type {
+  CopyDashboardRequest,
+  CreateDashboardRequest,
+  Dashboard,
+  DashboardId,
+  GetDashboardRequest,
+  ListDashboardsRequest,
+  SaveDashboardRequest,
+  UpdateDashboardRequest,
+} from "metabase-types/api";
+
+import { Api } from "./api";
+import {
+  idTag,
+  invalidateTags,
+  listTag,
+  provideDashboardListTags,
+  provideDashboardTags,
+} from "./tags";
+
+export const dashboardApi = Api.injectEndpoints({
+  endpoints: builder => ({
+    listDashboards: builder.query<Dashboard[], ListDashboardsRequest | void>({
+      query: body => ({
+        method: "GET",
+        url: "/api/dashboard",
+        body,
+      }),
+      providesTags: dashboards =>
+        dashboards ? provideDashboardListTags(dashboards) : [],
+    }),
+    getDashboard: builder.query<Dashboard, GetDashboardRequest>({
+      query: ({ id, ignore_error }) => ({
+        method: "GET",
+        url: `/api/dashboard/${id}`,
+        noEvent: ignore_error,
+      }),
+      providesTags: dashboard =>
+        dashboard ? provideDashboardTags(dashboard) : [],
+    }),
+    createDashboard: builder.mutation<Dashboard, CreateDashboardRequest>({
+      query: body => ({
+        method: "POST",
+        url: "/api/dashboard",
+        body,
+      }),
+      invalidatesTags: (_, error) =>
+        invalidateTags(error, [listTag("dashboard")]),
+    }),
+    updateDashboard: builder.mutation<Dashboard, UpdateDashboardRequest>({
+      query: ({ id, ...body }) => ({
+        method: "PUT",
+        url: `/api/dashboard/${id}`,
+        body,
+      }),
+      invalidatesTags: (_, error, { id }) =>
+        invalidateTags(error, [listTag("dashboard"), idTag("dashboard", id)]),
+    }),
+    deleteDashboard: builder.mutation<void, DashboardId>({
+      query: id => ({
+        method: "DELETE",
+        url: `/api/dashboard/${id}`,
+      }),
+      invalidatesTags: (_, error, id) =>
+        invalidateTags(error, [listTag("dashboard"), idTag("dashboard", id)]),
+    }),
+    saveDashboard: builder.mutation<Dashboard, SaveDashboardRequest>({
+      query: body => ({
+        method: "POST",
+        url: `/api/dashboard/save`,
+        body,
+      }),
+      invalidatesTags: (_, error) =>
+        invalidateTags(error, [listTag("dashboard")]),
+    }),
+    copyDashboard: builder.mutation<Dashboard, CopyDashboardRequest>({
+      query: ({ id, ...body }) => ({
+        method: "POST",
+        url: `/api/dashboard/${id}/copy`,
+        body,
+      }),
+      invalidatesTags: (_, error) =>
+        invalidateTags(error, [listTag("dashboard")]),
+    }),
+  }),
+});
+
+export const {
+  useCopyDashboardMutation,
+  useCreateDashboardMutation,
+  useDeleteDashboardMutation,
+  useGetDashboardQuery,
+  useListDashboardsQuery,
+  useSaveDashboardMutation,
+  useUpdateDashboardMutation,
+} = dashboardApi;
diff --git a/frontend/src/metabase/api/index.ts b/frontend/src/metabase/api/index.ts
index 57b5a083a165ebb5034fd56920eda93a6bb1f998..3afdb03479716b584ca23da737b3472a28c7bfad 100644
--- a/frontend/src/metabase/api/index.ts
+++ b/frontend/src/metabase/api/index.ts
@@ -6,6 +6,7 @@ export * from "./automagic-dashboards";
 export * from "./card";
 export * from "./collection";
 export * from "./database";
+export * from "./dashboard";
 export * from "./bookmark";
 export * from "./dataset";
 export * from "./field";
diff --git a/frontend/src/metabase/api/tags/utils.ts b/frontend/src/metabase/api/tags/utils.ts
index d00a19569670715a975122f8bd2cf14cbbb8547c..4dfd787865980d0d9cf923f523e0e1f42cb99d8c 100644
--- a/frontend/src/metabase/api/tags/utils.ts
+++ b/frontend/src/metabase/api/tags/utils.ts
@@ -1,5 +1,6 @@
 import type { TagDescription } from "@reduxjs/toolkit/query";
 
+import { isVirtualDashCard } from "metabase/dashboard/utils";
 import type {
   Alert,
   ApiKey,
@@ -8,6 +9,7 @@ import type {
   Collection,
   CollectionItem,
   CollectionItemModel,
+  Dashboard,
   Database,
   DatabaseCandidate,
   Field,
@@ -56,6 +58,10 @@ export function invalidateTags(
   return !error ? tags : [];
 }
 
+// ----------------------------------------------------------------------- //
+// Keep the below list of entity-specific functions alphabetically sorted. //
+// ----------------------------------------------------------------------- //
+
 export function provideActivityItemListTags(
   items: RecentItem[] | PopularItem[],
 ): TagDescription<TagType>[] {
@@ -94,36 +100,6 @@ export function provideApiKeyTags(apiKey: ApiKey): TagDescription<TagType>[] {
   return [idTag("api-key", apiKey.id)];
 }
 
-export function provideDatabaseCandidateListTags(
-  candidates: DatabaseCandidate[],
-): TagDescription<TagType>[] {
-  return [
-    listTag("schema"),
-    ...candidates.flatMap(provideDatabaseCandidateTags),
-  ];
-}
-
-export function provideDatabaseCandidateTags(
-  candidate: DatabaseCandidate,
-): TagDescription<TagType>[] {
-  return [idTag("schema", candidate.schema)];
-}
-
-export function provideDatabaseListTags(
-  databases: Database[],
-): TagDescription<TagType>[] {
-  return [listTag("database"), ...databases.flatMap(provideDatabaseTags)];
-}
-
-export function provideDatabaseTags(
-  database: Database,
-): TagDescription<TagType>[] {
-  return [
-    idTag("database", database.id),
-    ...(database.tables ? provideTableListTags(database.tables) : []),
-  ];
-}
-
 export function provideBookmarkListTags(
   bookmarks: Bookmark[],
 ): TagDescription<TagType>[] {
@@ -172,6 +148,58 @@ export function provideCollectionTags(
   return [idTag("collection", collection.id)];
 }
 
+export function provideDatabaseCandidateListTags(
+  candidates: DatabaseCandidate[],
+): TagDescription<TagType>[] {
+  return [
+    listTag("schema"),
+    ...candidates.flatMap(provideDatabaseCandidateTags),
+  ];
+}
+
+export function provideDatabaseCandidateTags(
+  candidate: DatabaseCandidate,
+): TagDescription<TagType>[] {
+  return [idTag("schema", candidate.schema)];
+}
+
+export function provideDatabaseListTags(
+  databases: Database[],
+): TagDescription<TagType>[] {
+  return [listTag("database"), ...databases.flatMap(provideDatabaseTags)];
+}
+
+export function provideDatabaseTags(
+  database: Database,
+): TagDescription<TagType>[] {
+  return [
+    idTag("database", database.id),
+    ...(database.tables ? provideTableListTags(database.tables) : []),
+  ];
+}
+
+export function provideDashboardListTags(
+  dashboards: Dashboard[],
+): TagDescription<TagType>[] {
+  return [listTag("dashboard"), ...dashboards.flatMap(provideDashboardTags)];
+}
+
+export function provideDashboardTags(
+  dashboard: Dashboard,
+): TagDescription<TagType>[] {
+  const cards = dashboard.dashcards
+    .flatMap(dashcard => (isVirtualDashCard(dashcard) ? [] : [dashcard]))
+    .map(dashcard => dashcard.card);
+
+  return [
+    idTag("dashboard", dashboard.id),
+    ...provideCardListTags(cards),
+    ...(dashboard.collection
+      ? provideCollectionTags(dashboard.collection)
+      : []),
+  ];
+}
+
 export function provideFieldListTags(
   fields: Field[],
 ): TagDescription<TagType>[] {
diff --git a/frontend/src/metabase/common/hooks/entity-framework/use-dashboard-query/use-dashboard-query.unit.spec.tsx b/frontend/src/metabase/common/hooks/entity-framework/use-dashboard-query/use-dashboard-query.unit.spec.tsx
index 969bd31e01bfbb8e2fb909bd837481c4537a4077..50fb95945c5d417af285a97f2a22fa2f1b2f9e93 100644
--- a/frontend/src/metabase/common/hooks/entity-framework/use-dashboard-query/use-dashboard-query.unit.spec.tsx
+++ b/frontend/src/metabase/common/hooks/entity-framework/use-dashboard-query/use-dashboard-query.unit.spec.tsx
@@ -31,7 +31,7 @@ const setup = () => {
   renderWithProviders(<TestComponent />);
 };
 
-describe("useDatabaseQuery", () => {
+describe("useDashboardQuery", () => {
   it("should be initially loading", () => {
     setup();
     expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
diff --git a/frontend/src/metabase/dashboard/containers/ArchiveDashboardModal.jsx b/frontend/src/metabase/dashboard/containers/ArchiveDashboardModal.jsx
index e2e188a58463c11ac6c8aaa77473560a154177fc..9f7cfab7e49c6addb5dfefdb37332cc64ccf9c9d 100644
--- a/frontend/src/metabase/dashboard/containers/ArchiveDashboardModal.jsx
+++ b/frontend/src/metabase/dashboard/containers/ArchiveDashboardModal.jsx
@@ -9,11 +9,11 @@ import _ from "underscore";
 
 import ArchiveModal from "metabase/components/ArchiveModal";
 import Collection from "metabase/entities/collections";
-import Dashboard from "metabase/entities/dashboards";
+import Dashboards from "metabase/entities/dashboards";
 import * as Urls from "metabase/lib/urls";
 
 const mapDispatchToProps = {
-  setDashboardArchived: Dashboard.actions.setArchived,
+  setDashboardArchived: Dashboards.actions.setArchived,
   push,
 };
 
@@ -56,7 +56,7 @@ class ArchiveDashboardModal extends Component {
 
 export const ArchiveDashboardModalConnected = _.compose(
   connect(null, mapDispatchToProps),
-  Dashboard.load({
+  Dashboards.load({
     id: (state, props) => Urls.extractCollectionId(props.params.slug),
   }),
   Collection.load({
diff --git a/frontend/src/metabase/entities/dashboards.js b/frontend/src/metabase/entities/dashboards.js
index 3fd372e5e014bcc915d2284bd269f55d170f64fc..54acc8623845d8ce9c4c2f3962398fa739672f2e 100644
--- a/frontend/src/metabase/entities/dashboards.js
+++ b/frontend/src/metabase/entities/dashboards.js
@@ -1,14 +1,17 @@
-import { assocIn } from "icepick";
 import { t } from "ttag";
 
+import { dashboardApi } from "metabase/api";
 import { canonicalCollectionId } from "metabase/collections/utils";
 import {
   getCollectionType,
   normalizedCollection,
 } from "metabase/entities/collections";
-import { POST, DELETE } from "metabase/lib/api";
 import { color } from "metabase/lib/colors";
-import { createEntity, undo } from "metabase/lib/entities";
+import {
+  createEntity,
+  entityCompatibleQuery,
+  undo,
+} from "metabase/lib/entities";
 import {
   compose,
   withAction,
@@ -20,8 +23,6 @@ import { addUndo } from "metabase/redux/undo";
 
 import forms from "./dashboards/forms";
 
-const FAVORITE_ACTION = `metabase/entities/dashboards/FAVORITE`;
-const UNFAVORITE_ACTION = `metabase/entities/dashboards/UNFAVORITE`;
 const COPY_ACTION = `metabase/entities/dashboards/COPY`;
 
 /**
@@ -36,10 +37,48 @@ const Dashboards = createEntity({
   displayNameMany: t`dashboards`,
 
   api: {
-    favorite: POST("/api/dashboard/:id/favorite"),
-    unfavorite: DELETE("/api/dashboard/:id/favorite"),
-    save: POST("/api/dashboard/save"),
-    copy: POST("/api/dashboard/:id/copy"),
+    list: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        dashboardApi.endpoints.listDashboards,
+      ),
+    get: (entityQuery, options, dispatch) =>
+      entityCompatibleQuery(
+        { ...entityQuery, ignore_error: options?.noEvent },
+        dispatch,
+        dashboardApi.endpoints.getDashboard,
+      ),
+    create: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        dashboardApi.endpoints.createDashboard,
+      ),
+    update: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        dashboardApi.endpoints.updateDashboard,
+      ),
+    delete: ({ id }, dispatch) =>
+      entityCompatibleQuery(
+        id,
+        dispatch,
+        dashboardApi.endpoints.deleteDashboard,
+      ),
+    save: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        dashboardApi.endpoints.saveDashboard,
+      ),
+    copy: (entityQuery, dispatch) =>
+      entityCompatibleQuery(
+        entityQuery,
+        dispatch,
+        dashboardApi.endpoints.copyDashboard,
+      ),
   },
 
   objectActions: {
@@ -67,16 +106,6 @@ const Dashboards = createEntity({
         opts,
       ),
 
-    setFavorited: async ({ id }, favorite) => {
-      if (favorite) {
-        await Dashboards.api.favorite({ id });
-        return { type: FAVORITE_ACTION, payload: id };
-      } else {
-        await Dashboards.api.unfavorite({ id });
-        return { type: UNFAVORITE_ACTION, payload: id };
-      }
-    },
-
     // TODO move into more common area as copy is implemented for more entities
     copy: compose(
       withAction(COPY_ACTION),
@@ -92,11 +121,15 @@ const Dashboards = createEntity({
       (entityObject, overrides, { notify } = {}) =>
         async (dispatch, getState) => {
           const result = Dashboards.normalize(
-            await Dashboards.api.copy({
-              id: entityObject.id,
-              ...overrides,
-              is_deep_copy: !overrides.is_shallow_copy,
-            }),
+            await entityCompatibleQuery(
+              {
+                id: entityObject.id,
+                ...overrides,
+                is_deep_copy: !overrides.is_shallow_copy,
+              },
+              dispatch,
+              dashboardApi.endpoints.copyDashboard,
+            ),
           );
           if (notify) {
             dispatch(addUndo(notify));
@@ -109,7 +142,11 @@ const Dashboards = createEntity({
 
   actions: {
     save: dashboard => async dispatch => {
-      const savedDashboard = await Dashboards.api.save(dashboard);
+      const savedDashboard = await entityCompatibleQuery(
+        dashboard,
+        dispatch,
+        dashboardApi.endpoints.saveDashboard,
+      );
       dispatch({ type: Dashboards.actionTypes.INVALIDATE_LISTS_ACTION });
       return {
         type: "metabase/entities/dashboards/SAVE_DASHBOARD",
@@ -119,18 +156,13 @@ const Dashboards = createEntity({
   },
 
   reducer: (state = {}, { type, payload, error }) => {
-    if (type === FAVORITE_ACTION && !error) {
-      return assocIn(state, [payload, "favorite"], true);
-    } else if (type === UNFAVORITE_ACTION && !error) {
-      return assocIn(state, [payload, "favorite"], false);
-    } else if (type === COPY_ACTION && !error && state[""]) {
+    if (type === COPY_ACTION && !error && state[""]) {
       return { ...state, "": state[""].concat([payload.result]) };
     }
     return state;
   },
 
   objectSelectors: {
-    getFavorited: dashboard => dashboard && dashboard.favorite,
     getName: dashboard => dashboard && dashboard.name,
     getUrl: dashboard => dashboard && Urls.dashboard(dashboard),
     getCollection: dashboard =>
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index c799f8328c9afdda7f0c7e84fbbcc7c5b885a6b9..ac6dcd29546b12fdd4db6ac207b81db817f3f530 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -182,8 +182,6 @@ export const DashboardApi = {
   get: GET("/api/dashboard/:dashId"),
   update: PUT("/api/dashboard/:id"),
   delete: DELETE("/api/dashboard/:dashId"),
-  favorite: POST("/api/dashboard/:dashId/favorite"),
-  unfavorite: DELETE("/api/dashboard/:dashId/favorite"),
   parameterValues: GET("/api/dashboard/:dashId/params/:paramId/values"),
   parameterSearch: GET("/api/dashboard/:dashId/params/:paramId/search/:query"),
   validFilterFields: GET("/api/dashboard/params/valid-filter-fields"),