Custom Post Types (CPTs) are where WordPress transitions from blogging platform to application framework. Used well, they let you model almost any data structure cleanly. Used carelessly, they create a maintenance nightmare.
What a CPT actually is
Everything in WordPress is a post. Pages are posts. Attachments are posts. A CPT is just a post with a custom post_type value. It gets its own admin menu, its own template files, and its own query arguments.
When to use a CPT
Use a CPT when you have a distinct content type with its own admin workflow, its own display template, and structured metadata beyond title and content. Good examples: Projects, Team Members, Testimonials, Events, Case Studies.
Bad examples: anything you could model with a category or tag, or anything where a simple custom field on an existing post type would do.
Register it properly
register_post_type( 'project', [
'labels' => [ 'name' => 'Projects', 'singular_name' => 'Project' ],
'public' => true,
'has_archive' => true,
'show_in_rest' => true,
'supports' => [ 'title', 'editor', 'thumbnail' ],
'menu_icon' => 'dashicons-portfolio',
'rewrite' => [ 'slug' => 'projects' ],
] );
Two things people often skip: show_in_rest => true (required for Gutenberg) and a sensible rewrite slug.
Metadata: post meta vs custom tables
WordPress stores CPT metadata in wp_postmeta as key-value pairs. This works well for small volumes. For CPTs with hundreds of thousands of rows or complex relational queries, consider a custom table. For most projects under 10,000 records, post meta with a well-named key prefix is completely fine.
The mistake that hurts later
The most common mistake: using a CPT when you should use a taxonomy, or vice versa. The rule is simple — if an item is a thing (a project, a person, an event), it’s a CPT. If it’s a label that groups things (a category, a status, a skill), it’s a taxonomy. Getting this right early saves significant refactoring time later.