Custom Post Type in WordPress lets you go beyond standard posts and pages to create structured content tailored to your site’s specific needs—like portfolios, testimonials, events, or products. By carefully configuring rewrite rules, enabling the REST API, and ensuring a good user interface (UI) in the WordPress admin, you make your custom post types feel like a natural part of WordPress.
In this article, we will walk you through how to create a custom post type programmatically, integrate it with WordPress rewrite rules, ensure it works seamlessly with the REST API (for block editor and external applications), and make it user-friendly in the WordPress dashboard. You’ll learn best practices, see code examples, and get tips on avoiding common pitfalls.
By default, WordPress includes several post types, such as post and page. However, not all content fits neatly into these categories. For instance, if you’re running a movie review site, you might want a “Movie” post type. If you manage an online directory, you might need a “Listing” post type. Custom Post Types let you structure your content for better organization, display, and management.
Defining your own CPT is as simple as calling the register_post_type() function, usually hooked to the init action. Once registered, your CPT can appear in the WordPress admin menu, be queryable on the front end, have its custom capabilities, and be integrated into the site’s permalink structure.

Below is a basic code snippet demonstrating how to register a CPT called my_custom_post. In a real scenario, you would place this code in a plugin file or your theme’s functions.php file. Using a plugin is often considered best practice, as it keeps functionality separate from the theme.
<?php
/**
* Plugin Name: My Custom Post Type Plugin
* Description: Registers a custom post type "My Custom Post".
*/
// Register the CPT on init
add_action( 'init', 'myplugin_register_my_custom_post_type' );
function myplugin_register_my_custom_post_type() {
register_post_type( 'my_custom_post', [
'label' => __( 'My Custom Posts', 'textdomain' ),
'public' => true,
'show_in_rest' => true, // Enable REST API support
'rewrite' => [ 'slug' => 'custom' ],
'supports' => [ 'title', 'editor', 'thumbnail', 'custom-fields' ],
'menu_icon' => 'dashicons-admin-post',
'menu_position' => 5,
'has_archive' => true,
'labels' => [
'name' => __( 'My Custom Posts', 'textdomain' ),
'singular_name' => __( 'My Custom Post', 'textdomain' ),
'add_new_item' => __( 'Add New Custom Post', 'textdomain' ),
'edit_item' => __( 'Edit Custom Post', 'textdomain' ),
'new_item' => __( 'New Custom Post', 'textdomain' ),
'view_item' => __( 'View Custom Post', 'textdomain' ),
'search_items' => __( 'Search Custom Posts', 'textdomain' ),
'not_found' => __( 'No custom posts found.', 'textdomain' ),
'not_found_in_trash' => __( 'No custom posts found in Trash.', 'textdomain' ),
],
] );
}
In this code:

If you visit your new custom post type’s single post page and see a 404 error, don’t panic. You might need to flush rewrite rules. The easiest manual method is going to Settings > Permalinks in WordPress and clicking “Save Changes” without modifying anything. This process refreshes the rewrite rules.
To do it programmatically, use flush_rewrite_rules() on plugin activation and deactivation:
register_activation_hook( __FILE__, 'myplugin_activate' );
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );
function myplugin_activate() {
myplugin_register_my_custom_post_type();
flush_rewrite_rules();
}
function myplugin_deactivate() {
flush_rewrite_rules();
}
Important: Don’t call flush_rewrite_rules() on every page load or after the init hook each time—this can harm performance. Instead, do it once upon activation or after changes to rewrite rules.

show_in_rest => true is your entry ticket to the WordPress REST API. This flag makes your custom post type accessible via endpoints like:
https://example.com/wp-json/wp/v2/my_custom_post
This integration is crucial if you:
Additional REST API customization options:
For most cases, show_in_rest => true is enough, but the REST API is highly customizable if you need more control.
Part of properly integrating your CPT is making sure it’s easy for administrators to use. Consider the following:
Taxonomies:
If your CPT should be organized by categories, tags, or custom taxonomies, register those taxonomies and associate them with the CPT. For example:
register_taxonomy( 'custom_category', 'my_custom_post', [
'label' => __( 'Custom Categories', 'textdomain' ),
'hierarchical' => true,
'public' => true,
'rewrite' => [ 'slug' => 'custom-category' ],
'show_in_rest' => true,
] );

Let’s expand the previous example to include a taxonomy, custom capabilities, and a more extensive set of arguments:
add_action( 'init', 'myplugin_register_movie_cpt' );
function myplugin_register_movie_cpt() {
// Register a taxonomy for genres
register_taxonomy( 'genre', 'movie', [
'label' => __( 'Genres', 'my-plugin' ),
'public' => true,
'hierarchical' => true,
'rewrite' => [ 'slug' => 'genre' ],
'show_in_rest' => true,
] );
// Register the movie CPT
register_post_type( 'movie', [
'label' => __( 'Movies', 'my-plugin' ),
'public' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'movies' ],
'menu_icon' => 'dashicons-format-video',
'has_archive' => true,
'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ],
'capability_type' => 'post', // or define custom capabilities for more control
'taxonomies' => [ 'genre' ],
'labels' => [
'name' => __( 'Movies', 'my-plugin' ),
'singular_name' => __( 'Movie', 'my-plugin' ),
'add_new_item' => __( 'Add New Movie', 'my-plugin' ),
'edit_item' => __( 'Edit Movie', 'my-plugin' ),
'new_item' => __( 'New Movie', 'my-plugin' ),
'view_item' => __( 'View Movie', 'my-plugin' ),
'search_items' => __( 'Search Movies', 'my-plugin' ),
'not_found' => __( 'No movies found.', 'my-plugin' ),
'not_found_in_trash' => __( 'No movies found in Trash.', 'my-plugin' ),
],
] );
}
This CPT, named “movie,” comes with a “genre” taxonomy, sets a custom slug (movies), supports a variety of features, and uses the REST API. It will appear in the WordPress admin with a video icon and a standard set of labels.

The ability to create a new post type in WordPress means there is no set way to how your content can be organized and displayed. By taking the right steps—such as registering it on the init hook, setting rewrite rules, enabling show_in_rest, and choosing the right UI settings—you seamlessly integrate your CPT into the WordPress ecosystem. This results in a fully customized, user-friendly content type that is easy to work with pretty permalinks, REST API, and the block editor.
Your new CPT can now serve your site’s unique content needs, whether you’re building a portfolio, managing events, listing products, or publishing a specialized type of content. By following best practices—like placing CPT code in a plugin, testing thoroughly, and avoiding unnecessary rewrite rule flushes—you ensure a stable, maintainable environment.
God willing, this guide helps you confidently create and integrate custom post types, making WordPress an even more powerful and flexible platform for your projects.

Hassan Tahir wrote this article, drawing on his experience to clarify WordPress concepts and enhance developers’ understanding. Through his work, he aims to help both beginners and professionals refine their skills and tackle WordPress projects with greater confidence.