diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md
index 3353b1dd41c248..aa493c7dcd997d 100644
--- a/docs/reference-guides/data/data-core.md
+++ b/docs/reference-guides/data/data-core.md
@@ -94,6 +94,30 @@ _Returns_
 
 -   `?Array`: An array of autosaves for the post, or undefined if there is none.
 
+### getBlockPatternCategories
+
+Retrieve the list of registered block pattern categories.
+
+_Parameters_
+
+-   _state_ `Object`: Data state.
+
+_Returns_
+
+-   `Array`: Block pattern category list.
+
+### getBlockPatterns
+
+Retrieve the list of registered block patterns.
+
+_Parameters_
+
+-   _state_ `Object`: Data state.
+
+_Returns_
+
+-   `Array`: Block pattern list.
+
 ### getCurrentTheme
 
 Return the current theme.
diff --git a/lib/compat/wordpress-5.9/block-patterns.php b/lib/compat/wordpress-5.9/block-patterns.php
index 46fed5f2a26cc1..b79869ea39bd2b 100644
--- a/lib/compat/wordpress-5.9/block-patterns.php
+++ b/lib/compat/wordpress-5.9/block-patterns.php
@@ -17,9 +17,11 @@ function _load_remote_featured_patterns() {
 		 *
 		 * @param bool $should_load_remote
 		 */
-		$should_load_remote = apply_filters( 'should_load_remote_block_patterns', true );
+		if ( ! apply_filters( 'should_load_remote_block_patterns', true ) ) {
+			return;
+		}
 
-		if ( ! $should_load_remote ) {
+		if ( ! get_theme_support( 'core-block-patterns' ) ) {
 			return;
 		}
 
@@ -43,18 +45,4 @@ function _load_remote_featured_patterns() {
 			}
 		}
 	}
-
-	add_action(
-		'current_screen',
-		function( $current_screen ) {
-			if ( ! get_theme_support( 'core-block-patterns' ) ) {
-				return;
-			}
-
-			$is_site_editor = ( function_exists( 'gutenberg_is_edit_site_page' ) && gutenberg_is_edit_site_page( $current_screen->id ) );
-			if ( $current_screen->is_block_editor || $is_site_editor ) {
-				_load_remote_featured_patterns();
-			}
-		}
-	);
 }
diff --git a/lib/compat/wordpress-6.0/block-editor-settings.php b/lib/compat/wordpress-6.0/block-editor-settings.php
index 7a8b5091a279e6..c070f64c7bf513 100644
--- a/lib/compat/wordpress-6.0/block-editor-settings.php
+++ b/lib/compat/wordpress-6.0/block-editor-settings.php
@@ -166,3 +166,18 @@ function_exists( 'gutenberg_is_edit_site_page' ) &&
 }
 
 add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings', PHP_INT_MAX );
+
+/**
+ * Removes the unwanted block patterns fields from block editor settings.
+ *
+ * @param array $settings Existing block editor settings.
+ *
+ * @return array New block editor settings.
+ */
+function gutenberg_remove_block_patterns_settings( $settings ) {
+	unset( $settings['__experimentalBlockPatterns'] );
+	unset( $settings['__experimentalBlockPatternCategories'] );
+	return $settings;
+}
+
+add_filter( 'block_editor_settings_all', 'gutenberg_remove_block_patterns_settings', PHP_INT_MAX );
diff --git a/lib/compat/wordpress-6.0/block-patterns.php b/lib/compat/wordpress-6.0/block-patterns.php
index b9ca9107fe413c..6c7517888124dd 100644
--- a/lib/compat/wordpress-6.0/block-patterns.php
+++ b/lib/compat/wordpress-6.0/block-patterns.php
@@ -10,10 +10,23 @@
  * `theme.json` file.
  */
 function gutenberg_register_remote_theme_patterns() {
+	if ( ! get_theme_support( 'core-block-patterns' ) ) {
+		return;
+	}
+
+	if ( ! apply_filters( 'should_load_remote_block_patterns', true ) ) {
+		return;
+	}
+
+	if ( ! WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) {
+		return;
+	}
+
 	$pattern_settings = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_patterns();
 	if ( empty( $pattern_settings ) ) {
 		return;
 	}
+
 	$request         = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' );
 	$request['slug'] = implode( ',', $pattern_settings );
 	$response        = rest_do_request( $request );
@@ -32,26 +45,6 @@ function gutenberg_register_remote_theme_patterns() {
 	}
 }
 
-add_action(
-	'current_screen',
-	function( $current_screen ) {
-		if ( ! get_theme_support( 'core-block-patterns' ) ) {
-			return;
-		}
-		if ( ! apply_filters( 'should_load_remote_block_patterns', true ) ) {
-			return;
-		}
-		if ( ! WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) {
-			return;
-		}
-
-		$is_site_editor = ( function_exists( 'gutenberg_is_edit_site_page' ) && gutenberg_is_edit_site_page( $current_screen->id ) );
-		if ( $current_screen->is_block_editor || $is_site_editor ) {
-			gutenberg_register_remote_theme_patterns();
-		}
-	}
-);
-
 /**
  * Register any patterns that the active theme may provide under its
  * `./patterns/` directory. Each pattern is defined as a PHP file and defines
diff --git a/lib/compat/wordpress-6.0/class-wp-rest-block-pattern-categories-controller.php b/lib/compat/wordpress-6.0/class-wp-rest-block-pattern-categories-controller.php
new file mode 100644
index 00000000000000..8edc2080b33dd7
--- /dev/null
+++ b/lib/compat/wordpress-6.0/class-wp-rest-block-pattern-categories-controller.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * REST API: WP_REST_Block_Pattern_Catergories_Controller class
+ *
+ * @subpackage REST_API
+ * @package    WordPress
+ */
+
+/**
+ * Core class used to access block pattern categories via the REST API.
+ *
+ * @see   WP_REST_Controller
+ */
+class WP_REST_Block_Pattern_Categories_Controller extends WP_REST_Controller {
+
+	/**
+	 * Constructor.
+	 */
+	public function __construct() {
+		$this->namespace = '__experimental';
+		$this->rest_base = 'block-patterns/categories';
+	}
+
+	/**
+	 * Registers the routes for the objects of the controller.
+	 *
+	 * @see   register_rest_route()
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			array(
+				array(
+					'methods'             => WP_REST_Server::READABLE,
+					'callback'            => array( $this, 'get_items' ),
+					'permission_callback' => array( $this, 'get_items_permissions_check' ),
+				),
+				'schema'      => array( $this, 'get_public_item_schema' ),
+				'allow_batch' => array( 'v1' => true ),
+			)
+		);
+	}
+
+	/**
+	 * Checks whether a given request has permission to read block patterns.
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
+	 */
+	public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+		if ( current_user_can( 'edit_posts' ) ) {
+			return true;
+		}
+
+		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
+			if ( current_user_can( $post_type->cap->edit_posts ) ) {
+				return true;
+			}
+		}
+
+		return new WP_Error(
+			'rest_cannot_view',
+			__( 'Sorry, you are not allowed to view the registered block pattern categories.', 'gutenberg' ),
+			array( 'status' => rest_authorization_required_code() )
+		);
+	}
+
+	/**
+	 * Retrieves all block pattern categories.
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
+	 */
+	public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+		$response   = array();
+		$categories = WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered();
+		foreach ( $categories as $category ) {
+			$prepared_category = $this->prepare_item_for_response( $category, $request );
+			$response[]        = $this->prepare_response_for_collection( $prepared_category );
+		}
+		return rest_ensure_response( $response );
+	}
+
+	/**
+	 * Prepare a raw block pattern category before it gets output in a REST API response.
+	 *
+	 * @param object          $item    Raw category as registered, before any changes.
+	 * @param WP_REST_Request $request Request object.
+	 * @return WP_REST_Response
+	 */
+	public function prepare_item_for_response( $item, $request ) {
+		$fields = $this->get_fields_for_response( $request );
+		$keys   = array( 'name', 'label' );
+		$data   = array();
+		foreach ( $keys as $key ) {
+			if ( rest_is_field_included( $key, $fields ) ) {
+				$data[ $key ] = $item[ $key ];
+			}
+		}
+
+		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+		$data    = $this->add_additional_fields_to_object( $data, $request );
+		$data    = $this->filter_response_by_context( $data, $context );
+		return rest_ensure_response( $data );
+	}
+
+	/**
+	 * Retrieves the block pattern category schema, conforming to JSON Schema.
+	 *
+	 * @return array Item schema data.
+	 */
+	public function get_item_schema() {
+		$schema = array(
+			'$schema'    => 'http://json-schema.org/draft-04/schema#',
+			'title'      => 'block-pattern-category',
+			'type'       => 'object',
+			'properties' => array(
+				'name'  => array(
+					'description' => __( 'The category name.', 'gutenberg' ),
+					'type'        => 'string',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'label' => array(
+					'description' => __( 'The category label, in human readable format.', 'gutenberg' ),
+					'type'        => 'string',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+			),
+		);
+
+		return $this->add_additional_fields_schema( $schema );
+	}
+}
diff --git a/lib/compat/wordpress-6.0/class-wp-rest-block-patterns-controller.php b/lib/compat/wordpress-6.0/class-wp-rest-block-patterns-controller.php
new file mode 100644
index 00000000000000..f42e733a1f8246
--- /dev/null
+++ b/lib/compat/wordpress-6.0/class-wp-rest-block-patterns-controller.php
@@ -0,0 +1,188 @@
+<?php
+/**
+ * REST API: WP_REST_Block_Patterns_Controller class
+ *
+ * @subpackage REST_API
+ * @package    WordPress
+ */
+
+/**
+ * Core class used to access block patterns via the REST API.
+ *
+ * @see   WP_REST_Controller
+ */
+class WP_REST_Block_Patterns_Controller extends WP_REST_Controller {
+
+	/**
+	 * Constructor.
+	 */
+	public function __construct() {
+		$this->namespace = '__experimental';
+		$this->rest_base = 'block-patterns/patterns';
+	}
+
+	/**
+	 * Registers the routes for the objects of the controller.
+	 *
+	 * @see   register_rest_route()
+	 */
+	public function register_routes() {
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			array(
+				array(
+					'methods'             => WP_REST_Server::READABLE,
+					'callback'            => array( $this, 'get_items' ),
+					'permission_callback' => array( $this, 'get_items_permissions_check' ),
+				),
+				'schema'      => array( $this, 'get_public_item_schema' ),
+				'allow_batch' => array( 'v1' => true ),
+			)
+		);
+	}
+
+	/**
+	 * Checks whether a given request has permission to read block patterns.
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
+	 */
+	public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+		if ( current_user_can( 'edit_posts' ) ) {
+			return true;
+		}
+
+		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
+			if ( current_user_can( $post_type->cap->edit_posts ) ) {
+				return true;
+			}
+		}
+
+		return new WP_Error(
+			'rest_cannot_view',
+			__( 'Sorry, you are not allowed to view the registered block patterns.', 'gutenberg' ),
+			array( 'status' => rest_authorization_required_code() )
+		);
+	}
+
+	/**
+	 * Retrieves all block patterns.
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
+	 */
+	public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+		// Load block patterns from w.org.
+		_load_remote_block_patterns(); // Patterns with the `core` keyword.
+		_load_remote_featured_patterns(); // Patterns in the `featured` category.
+		gutenberg_register_remote_theme_patterns(); // Patterns requested by current theme.
+
+		$response = array();
+		$patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
+		foreach ( $patterns as $pattern ) {
+			$prepared_pattern = $this->prepare_item_for_response( $pattern, $request );
+			$response[]       = $this->prepare_response_for_collection( $prepared_pattern );
+		}
+		return rest_ensure_response( $response );
+	}
+
+	/**
+	 * Prepare a raw block pattern before it gets output in a REST API response.
+	 *
+	 * @param object          $item    Raw pattern as registered, before any changes.
+	 * @param WP_REST_Request $request Request object.
+	 * @return WP_REST_Response
+	 */
+	public function prepare_item_for_response( $item, $request ) {
+		$fields = $this->get_fields_for_response( $request );
+		$keys   = array(
+			'name',
+			'title',
+			'description',
+			'viewportWidth',
+			'blockTypes',
+			'categories',
+			'keywords',
+			'content',
+		);
+		$data   = array();
+		foreach ( $keys as $key ) {
+			if ( isset( $item[ $key ] ) && rest_is_field_included( $key, $fields ) ) {
+				$data[ $key ] = $item[ $key ];
+			}
+		}
+
+		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+		$data    = $this->add_additional_fields_to_object( $data, $request );
+		$data    = $this->filter_response_by_context( $data, $context );
+		return rest_ensure_response( $data );
+	}
+
+	/**
+	 * Retrieves the block pattern schema, conforming to JSON Schema.
+	 *
+	 * @return array Item schema data.
+	 */
+	public function get_item_schema() {
+		$schema = array(
+			'$schema'    => 'http://json-schema.org/draft-04/schema#',
+			'title'      => 'block-pattern',
+			'type'       => 'object',
+			'properties' => array(
+				'name'          => array(
+					'description' => __( 'The pattern name.', 'gutenberg' ),
+					'type'        => 'string',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'title'         => array(
+					'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ),
+					'type'        => 'string',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'description'   => array(
+					'description' => __( 'The pattern detailed description.', 'gutenberg' ),
+					'type'        => 'string',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'viewportWidth' => array(
+					'description' => __( 'The pattern viewport width for inserter preview.', 'gutenberg' ),
+					'type'        => 'number',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'blockTypes'    => array(
+					'description' => __( 'Block types that the pattern is intended to be used with.', 'gutenberg' ),
+					'type'        => 'array',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'categories'    => array(
+					'description' => __( 'The pattern category slugs.', 'gutenberg' ),
+					'type'        => 'array',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'keywords'      => array(
+					'description' => __( 'The pattern keywords.', 'gutenberg' ),
+					'type'        => 'array',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+				'content'       => array(
+					'description' => __( 'The pattern content.', 'gutenberg' ),
+					'type'        => 'string',
+					'readonly'    => true,
+					'context'     => array( 'view', 'embed' ),
+				),
+			),
+		);
+
+		return $this->add_additional_fields_schema( $schema );
+	}
+}
diff --git a/lib/compat/wordpress-6.0/edit-form-blocks.php b/lib/compat/wordpress-6.0/edit-form-blocks.php
index f4d4bbb5e546ed..68fc938e9e7420 100644
--- a/lib/compat/wordpress-6.0/edit-form-blocks.php
+++ b/lib/compat/wordpress-6.0/edit-form-blocks.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Patches preload paths for post editor.
+ * Patches resources loaded by the block editor page.
  *
  * @package gutenberg
  */
@@ -48,5 +48,19 @@ function optimize_preload_paths( $preload_paths ) {
 
 	return $preload_paths;
 }
-
 add_filter( 'block_editor_rest_api_preload_paths', 'optimize_preload_paths' );
+
+/**
+ * Disables loading remote block patterns from REST while initializing the editor.
+ * Nowadays these loads are done in the `block-patterns/patterns` REST endpoint, and
+ * are undesired when initializing the block editor page, both in post and site editor.
+ *
+ * @param WP_Screen $current_screen WordPress current screen object.
+ */
+function disable_load_remote_patterns( $current_screen ) {
+	$is_site_editor = ( function_exists( 'gutenberg_is_edit_site_page' ) && gutenberg_is_edit_site_page( $current_screen->id ) );
+	if ( $is_site_editor || $current_screen->is_block_editor() ) {
+		add_filter( 'should_load_remote_block_patterns', '__return_false' );
+	}
+}
+add_action( 'current_screen', 'disable_load_remote_patterns' );
diff --git a/lib/compat/wordpress-6.0/rest-api.php b/lib/compat/wordpress-6.0/rest-api.php
index da1f6977e73ca8..13e64dcf0ff132 100644
--- a/lib/compat/wordpress-6.0/rest-api.php
+++ b/lib/compat/wordpress-6.0/rest-api.php
@@ -34,3 +34,21 @@ function gutenberg_register_edit_site_export_endpoint() {
 	$editor_settings->register_routes();
 }
 add_action( 'rest_api_init', 'gutenberg_register_edit_site_export_endpoint' );
+
+/**
+ * Registers the block patterns REST API routes.
+ */
+function gutenberg_register_rest_block_patterns() {
+	$block_patterns = new WP_REST_Block_Patterns_Controller();
+	$block_patterns->register_routes();
+}
+add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' );
+
+/**
+ * Registers the block pattern categories REST API routes.
+ */
+function gutenberg_register_rest_block_pattern_categories() {
+	$block_patterns = new WP_REST_Block_Pattern_Categories_Controller();
+	$block_patterns->register_routes();
+}
+add_action( 'rest_api_init', 'gutenberg_register_rest_block_pattern_categories' );
diff --git a/lib/full-site-editing/edit-site-page.php b/lib/full-site-editing/edit-site-page.php
index 50ff0d0eb2b208..bd9bb17286a38a 100644
--- a/lib/full-site-editing/edit-site-page.php
+++ b/lib/full-site-editing/edit-site-page.php
@@ -114,14 +114,12 @@ static function( $classes ) {
 	}
 
 	$custom_settings = array(
-		'siteUrl'                              => site_url(),
-		'postsPerPage'                         => get_option( 'posts_per_page' ),
-		'styles'                               => gutenberg_get_editor_styles(),
-		'defaultTemplateTypes'                 => $indexed_template_types,
-		'defaultTemplatePartAreas'             => get_allowed_block_template_part_areas(),
-		'__unstableHomeTemplate'               => gutenberg_resolve_home_template(),
-		'__experimentalBlockPatterns'          => WP_Block_Patterns_Registry::get_instance()->get_all_registered(),
-		'__experimentalBlockPatternCategories' => WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered(),
+		'siteUrl'                  => site_url(),
+		'postsPerPage'             => get_option( 'posts_per_page' ),
+		'styles'                   => gutenberg_get_editor_styles(),
+		'defaultTemplateTypes'     => $indexed_template_types,
+		'defaultTemplatePartAreas' => get_allowed_block_template_part_areas(),
+		'__unstableHomeTemplate'   => gutenberg_resolve_home_template(),
 	);
 
 	/**
diff --git a/lib/load.php b/lib/load.php
index b72686b1f4c42c..0e2b3e2796ed71 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -97,6 +97,8 @@ function gutenberg_is_experiment_enabled( $name ) {
 require __DIR__ . '/compat/wordpress-6.0/class-gutenberg-rest-global-styles-controller.php';
 require __DIR__ . '/compat/wordpress-6.0/class-gutenberg-rest-pattern-directory-controller.php';
 require __DIR__ . '/compat/wordpress-6.0/class-gutenberg-rest-edit-site-export-controller.php';
+require __DIR__ . '/compat/wordpress-6.0/class-wp-rest-block-patterns-controller.php';
+require __DIR__ . '/compat/wordpress-6.0/class-wp-rest-block-pattern-categories-controller.php';
 require __DIR__ . '/compat/wordpress-6.0/class-wp-theme-json-gutenberg.php';
 require __DIR__ . '/compat/wordpress-6.0/rest-api.php';
 require __DIR__ . '/compat/wordpress-6.0/block-patterns.php';
diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md
index 4624ba5c9a6de4..4a7722232adb56 100644
--- a/packages/core-data/CHANGELOG.md
+++ b/packages/core-data/CHANGELOG.md
@@ -6,6 +6,7 @@
 
 ### New Features
 –   The saveEntityRecord, saveEditedEntityRecord, and deleteEntityRecord actions now accept an optional throwOnError option (defaults to false). When set to true, any exceptions occurring when the action was executing are re-thrown, causing dispatch().saveEntityRecord() to reject with an error. ([#39258](https://github.com/WordPress/gutenberg/pull/39258))
+-   Added support for fetching block patterns and their categories, with the `getBlockPatterns` and `getBlockPatternCategories` selectors.
 
 ## 4.2.0 (2022-03-11)
 
diff --git a/packages/core-data/README.md b/packages/core-data/README.md
index aa26746a4df95f..2dc7df4a04d71e 100644
--- a/packages/core-data/README.md
+++ b/packages/core-data/README.md
@@ -341,6 +341,30 @@ _Returns_
 
 -   `?Array`: An array of autosaves for the post, or undefined if there is none.
 
+### getBlockPatternCategories
+
+Retrieve the list of registered block pattern categories.
+
+_Parameters_
+
+-   _state_ `Object`: Data state.
+
+_Returns_
+
+-   `Array`: Block pattern category list.
+
+### getBlockPatterns
+
+Retrieve the list of registered block patterns.
+
+_Parameters_
+
+-   _state_ `Object`: Data state.
+
+_Returns_
+
+-   `Array`: Block pattern list.
+
 ### getCurrentTheme
 
 Return the current theme.
diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js
index 0c901c8ab7a2fc..16b265bcf892e6 100644
--- a/packages/core-data/src/reducer.js
+++ b/packages/core-data/src/reducer.js
@@ -592,6 +592,24 @@ export function autosaves( state = {}, action ) {
 	return state;
 }
 
+export function blockPatterns( state = [], action ) {
+	switch ( action.type ) {
+		case 'RECEIVE_BLOCK_PATTERNS':
+			return action.patterns;
+	}
+
+	return state;
+}
+
+export function blockPatternCategories( state = [], action ) {
+	switch ( action.type ) {
+		case 'RECEIVE_BLOCK_PATTERN_CATEGORIES':
+			return action.categories;
+	}
+
+	return state;
+}
+
 export default combineReducers( {
 	terms,
 	users,
@@ -606,4 +624,6 @@ export default combineReducers( {
 	embedPreviews,
 	userPermissions,
 	autosaves,
+	blockPatterns,
+	blockPatternCategories,
 } );
diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js
index fda58b2fe9ea87..ad7b56befca3e9 100644
--- a/packages/core-data/src/resolvers.js
+++ b/packages/core-data/src/resolvers.js
@@ -453,3 +453,17 @@ export const __experimentalGetCurrentThemeGlobalStylesVariations = () => async (
 		variations
 	);
 };
+
+export const getBlockPatterns = () => async ( { dispatch } ) => {
+	const patterns = await apiFetch( {
+		path: '/__experimental/block-patterns/patterns',
+	} );
+	dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } );
+};
+
+export const getBlockPatternCategories = () => async ( { dispatch } ) => {
+	const categories = await apiFetch( {
+		path: '/__experimental/block-patterns/categories',
+	} );
+	dispatch( { type: 'RECEIVE_BLOCK_PATTERN_CATEGORIES', categories } );
+};
diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js
index 01dc3024152afd..67cd6b1aa5087e 100644
--- a/packages/core-data/src/selectors.js
+++ b/packages/core-data/src/selectors.js
@@ -962,3 +962,25 @@ export function __experimentalGetCurrentThemeGlobalStylesVariations( state ) {
 	}
 	return state.themeGlobalStyleVariations[ currentTheme.stylesheet ];
 }
+
+/**
+ * Retrieve the list of registered block patterns.
+ *
+ * @param {Object} state Data state.
+ *
+ * @return {Array} Block pattern list.
+ */
+export function getBlockPatterns( state ) {
+	return state.blockPatterns;
+}
+
+/**
+ * Retrieve the list of registered block pattern categories.
+ *
+ * @param {Object} state Data state.
+ *
+ * @return {Array} Block pattern category list.
+ */
+export function getBlockPatternCategories( state ) {
+	return state.blockPatternCategories;
+}
diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js
index de18566e66fff4..d0e390165fd7d5 100644
--- a/packages/edit-site/src/components/block-editor/index.js
+++ b/packages/edit-site/src/components/block-editor/index.js
@@ -8,7 +8,7 @@ import classnames from 'classnames';
  */
 import { useSelect, useDispatch } from '@wordpress/data';
 import { useCallback, useRef } from '@wordpress/element';
-import { useEntityBlockEditor } from '@wordpress/core-data';
+import { useEntityBlockEditor, store as coreStore } from '@wordpress/core-data';
 import {
 	BlockList,
 	BlockEditorProvider,
@@ -42,17 +42,44 @@ const LAYOUT = {
 };
 
 export default function BlockEditor( { setIsInserterOpen } ) {
-	const { settings, templateType, templateId, page } = useSelect(
+	const { settings } = useSelect(
 		( select ) => {
-			const {
-				getSettings,
-				getEditedPostType,
-				getEditedPostId,
-				getPage,
-			} = select( editSiteStore );
+			let storedSettings = select( editSiteStore ).getSettings(
+				setIsInserterOpen
+			);
+
+			if ( ! storedSettings.__experimentalBlockPatterns ) {
+				storedSettings = {
+					...storedSettings,
+					__experimentalBlockPatterns: select(
+						coreStore
+					).getBlockPatterns(),
+				};
+			}
+
+			if ( ! storedSettings.__experimentalBlockPatternCategories ) {
+				storedSettings = {
+					...storedSettings,
+					__experimentalBlockPatternCategories: select(
+						coreStore
+					).getBlockPatternCategories(),
+				};
+			}
+
+			return {
+				settings: storedSettings,
+			};
+		},
+		[ setIsInserterOpen ]
+	);
+
+	const { templateType, templateId, page } = useSelect(
+		( select ) => {
+			const { getEditedPostType, getEditedPostId, getPage } = select(
+				editSiteStore
+			);
 
 			return {
-				settings: getSettings( setIsInserterOpen ),
 				templateType: getEditedPostType(),
 				templateId: getEditedPostId(),
 				page: getPage(),
@@ -60,6 +87,7 @@ export default function BlockEditor( { setIsInserterOpen } ) {
 		},
 		[ setIsInserterOpen ]
 	);
+
 	const [ blocks, onInput, onChange ] = useEntityBlockEditor(
 		'postType',
 		templateType
diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js
index 68849f85a6b53b..65f0577f9fd161 100644
--- a/packages/editor/src/components/provider/use-block-editor-settings.js
+++ b/packages/editor/src/components/provider/use-block-editor-settings.js
@@ -61,6 +61,22 @@ function useBlockEditorSettings( settings, hasTemplate ) {
 		};
 	}, [] );
 
+	const {
+		__experimentalBlockPatterns: settingsBlockPatterns,
+		__experimentalBlockPatternCategories: settingsBlockPatternCategories,
+	} = settings;
+
+	const { blockPatterns, blockPatternCategories } = useSelect(
+		( select ) => ( {
+			blockPatterns:
+				settingsBlockPatterns ?? select( coreStore ).getBlockPatterns(),
+			blockPatternCategories:
+				settingsBlockPatternCategories ??
+				select( coreStore ).getBlockPatternCategories(),
+		} ),
+		[ settingsBlockPatterns, settingsBlockPatternCategories ]
+	);
+
 	const { undo } = useDispatch( editorStore );
 
 	const { saveEntityRecord } = useDispatch( coreStore );
@@ -85,8 +101,6 @@ function useBlockEditorSettings( settings, hasTemplate ) {
 		() => ( {
 			...pick( settings, [
 				'__experimentalBlockDirectory',
-				'__experimentalBlockPatternCategories',
-				'__experimentalBlockPatterns',
 				'__experimentalDiscussionSettings',
 				'__experimentalFeatures',
 				'__experimentalPreferredStyleVariations',
@@ -128,6 +142,8 @@ function useBlockEditorSettings( settings, hasTemplate ) {
 			] ),
 			mediaUpload: hasUploadPermissions ? mediaUpload : undefined,
 			__experimentalReusableBlocks: reusableBlocks,
+			__experimentalBlockPatterns: blockPatterns,
+			__experimentalBlockPatternCategories: blockPatternCategories,
 			__experimentalFetchLinkSuggestions: ( search, searchOptions ) =>
 				fetchLinkSuggestions( search, searchOptions, settings ),
 			__experimentalFetchRichUrlData: fetchUrlData,
@@ -143,6 +159,8 @@ function useBlockEditorSettings( settings, hasTemplate ) {
 			settings,
 			hasUploadPermissions,
 			reusableBlocks,
+			blockPatterns,
+			blockPatternCategories,
 			canUseUnfilteredHTML,
 			undo,
 			hasTemplate,
diff --git a/phpunit/class-wp-rest-block-pattern-categories-controller-test.php b/phpunit/class-wp-rest-block-pattern-categories-controller-test.php
new file mode 100644
index 00000000000000..8f22b0eefad5bf
--- /dev/null
+++ b/phpunit/class-wp-rest-block-pattern-categories-controller-test.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Unit tests covering WP_REST_Block_Pattern_Categories_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ */
+
+/**
+ * Unit tests for REST API for Block Pattern Categories.
+ *
+ * @group restapi
+ * @covers WP_REST_Block_Pattern_Categories_Controller
+ */
+class WP_REST_Block_Pattern_Categories_Controller_Test extends WP_Test_REST_Controller_Testcase {
+	protected static $admin_id;
+	protected static $orig_registry;
+
+	public function set_up() {
+		parent::set_up();
+		switch_theme( 'emptytheme' );
+	}
+
+	public static function wpSetupBeforeClass( $factory ) {
+		// Create a test user.
+		self::$admin_id = $factory->user->create( array( 'role' => 'administrator' ) );
+
+		// Setup an empty testing instance of `WP_Block_Pattern_Categories_Registry` and save the original.
+		$reflection = new ReflectionClass( 'WP_Block_Pattern_Categories_Registry' );
+		$reflection->getProperty( 'instance' )->setAccessible( true );
+		self::$orig_registry = $reflection->getStaticPropertyValue( 'instance' );
+		$test_registry       = new WP_Block_Pattern_Categories_Registry();
+		$reflection->setStaticPropertyValue( 'instance', $test_registry );
+
+		// Register some categories in the test registry.
+		$test_registry->register( 'test', array( 'label' => 'Test' ) );
+		$test_registry->register( 'query', array( 'label' => 'Query' ) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		// Delete the test user.
+		self::delete_user( self::$admin_id );
+
+		// Restore the original registry instance.
+		$reflection = new ReflectionClass( 'WP_Block_Pattern_Categories_Registry' );
+		$reflection->setStaticPropertyValue( 'instance', self::$orig_registry );
+	}
+
+	public function test_register_routes() {
+		$routes = rest_get_server()->get_routes();
+		$this->assertArrayHasKey(
+			'/__experimental/block-patterns/categories',
+			$routes,
+			'The categories route does not exist'
+		);
+	}
+
+	public function test_get_items() {
+		wp_set_current_user( self::$admin_id );
+
+		$expected_names  = array( 'test', 'query' );
+		$expected_fields = array( 'name', 'label' );
+
+		$request            = new WP_REST_Request( 'GET', '/__experimental/block-patterns/categories' );
+		$request['_fields'] = 'name,label';
+		$response           = rest_get_server()->dispatch( $request );
+		$data               = $response->get_data();
+
+		$this->assertCount( count( $expected_names ), $data );
+		foreach ( $data as $idx => $item ) {
+			$this->assertEquals( $expected_names[ $idx ], $item['name'] );
+			$this->assertEquals( $expected_fields, array_keys( $item ) );
+		}
+	}
+
+	/**
+	 * Abstract methods that we must implement.
+	 */
+	public function test_context_param() {}
+	public function test_get_item() {}
+	public function test_create_item() {}
+	public function test_update_item() {}
+	public function test_delete_item() {}
+	public function test_prepare_item() {}
+	public function test_get_item_schema() {}
+}
diff --git a/phpunit/class-wp-rest-block-patterns-controller-test.php b/phpunit/class-wp-rest-block-patterns-controller-test.php
new file mode 100644
index 00000000000000..6db9f1ca499624
--- /dev/null
+++ b/phpunit/class-wp-rest-block-patterns-controller-test.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * Unit tests covering WP_REST_Block_Patterns_Controller functionality.
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ */
+
+/**
+ * Unit tests for REST API for Block Patterns.
+ *
+ * @group restapi
+ * @covers WP_REST_Block_Patterns_Controller
+ */
+class WP_REST_Block_Patterns_Controller_Test extends WP_Test_REST_Controller_Testcase {
+	protected static $admin_id;
+	protected static $orig_registry;
+
+	public function set_up() {
+		parent::set_up();
+		switch_theme( 'emptytheme' );
+	}
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		// Create a test user.
+		self::$admin_id = $factory->user->create( array( 'role' => 'administrator' ) );
+
+		// Setup an empty testing instance of `WP_Block_Patterns_Registry` and save the original.
+		$reflection = new ReflectionClass( 'WP_Block_Patterns_Registry' );
+		$reflection->getProperty( 'instance' )->setAccessible( true );
+		self::$orig_registry = $reflection->getStaticPropertyValue( 'instance' );
+		$test_registry       = new WP_Block_Patterns_Registry();
+		$reflection->setStaticPropertyValue( 'instance', $test_registry );
+
+		// Register some patterns in the test registry.
+		$test_registry->register(
+			'test/one',
+			array(
+				'title'         => 'Pattern One',
+				'categories'    => array( 'test' ),
+				'viewportWidth' => 1440,
+				'content'       => '<!-- wp:heading {"level":1} --><h1>One</h1><!-- /wp:heading -->',
+			)
+		);
+
+		$test_registry->register(
+			'test/two',
+			array(
+				'title'      => 'Pattern Two',
+				'categories' => array( 'test' ),
+				'content'    => '<!-- wp:paragraph --><p>Two</p><!-- /wp:paragraph -->',
+			)
+		);
+	}
+
+	public static function wpTearDownAfterClass() {
+		// Delete the test user.
+		self::delete_user( self::$admin_id );
+
+		// Restore the original registry instance.
+		$reflection = new ReflectionClass( 'WP_Block_Patterns_Registry' );
+		$reflection->setStaticPropertyValue( 'instance', self::$orig_registry );
+	}
+
+	public function test_register_routes() {
+		$routes = rest_get_server()->get_routes();
+		$this->assertArrayHasKey(
+			'/__experimental/block-patterns/patterns',
+			$routes,
+			'The patterns route does not exist'
+		);
+	}
+
+	public function test_get_items() {
+		wp_set_current_user( self::$admin_id );
+
+		$expected_names  = array( 'test/one', 'test/two' );
+		$expected_fields = array( 'name', 'content' );
+
+		$request            = new WP_REST_Request( 'GET', '/__experimental/block-patterns/patterns' );
+		$request['_fields'] = 'name,content';
+		$response           = rest_get_server()->dispatch( $request );
+		$data               = $response->get_data();
+
+		$this->assertCount( count( $expected_names ), $data );
+		foreach ( $data as $idx => $item ) {
+			$this->assertEquals( $expected_names[ $idx ], $item['name'] );
+			$this->assertEquals( $expected_fields, array_keys( $item ) );
+		}
+	}
+
+	/**
+	 * Abstract methods that we must implement.
+	 */
+	public function test_context_param() {}
+	public function test_get_item() {}
+	public function test_create_item() {}
+	public function test_update_item() {}
+	public function test_delete_item() {}
+	public function test_prepare_item() {}
+	public function test_get_item_schema() {}
+}