WordPress & multilingual sites
How to Build a WordPress Plugin: Structure, Hooks, Security and Testing
A practical walkthrough of how to build a WordPress plugin: structure, hooks, security rules, a working REST endpoint and testing, based on what we ship in production.
By AYM FlowPublished: Updated: 6 min read
Learning how to build a WordPress plugin is quick; building one that is small, predictable and safe takes discipline. A bad one is a pile of functions that breaks on the next update and opens a security hole. This guide covers the decisions that separate the two: when a plugin is the right tool, how to structure it, how to use hooks, how to secure input and output, how to expose a REST API, and how to test before release.
Decide whether you need a plugin
Put functionality in a plugin when it should survive a theme change: custom post types, integrations, business logic, shortcodes, blocks and anything tied to your data rather than your design. Presentation belongs in the theme. A must-use plugin suits site-wide rules that nobody should be able to deactivate. Before building, check whether a lean, well-maintained plugin already does the job, because every dependency has a maintenance cost either way.
How to build a WordPress plugin: start with a clean structure
WordPress only needs one PHP file with a header comment, but a maintainable plugin is organised from day one. A layout we use as a baseline:
- plugin-slug.php: the main file with the header, constants and a bootstrap call. Keep it thin.
- src/ or includes/: namespaced classes, one responsibility each, loaded through a Composer or PSR-4 style autoloader.
- assets/: scripts and styles, enqueued only on the screens that need them.
- languages/: translation files, with a consistent text domain.
- uninstall.php: removes options, tables and scheduled events when the plugin is deleted.
Add a guard at the top of every PHP file so it cannot be run directly, prefix or namespace everything to avoid collisions, and follow the WordPress PHP coding standards so any WordPress developer can read your code.
A minimal plugin that actually works
Here is a complete, working plugin in a single file. It shows the parts every plugin needs: the header WordPress reads, a guard against direct access, an option created on activation, a shortcode that escapes its output and a REST endpoint that checks permissions and sanitises input.
<?php
/**
* Plugin Name: Acme Notice
* Description: Shows an editable notice with the [acme_notice] shortcode.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Acme
* License: GPL-2.0-or-later
* Text Domain: acme-notice
*/
defined( 'ABSPATH' ) || exit; // no direct access
const ACME_NOTICE_OPTION = 'acme_notice_text';
// Create the option once, without autoloading it on every request.
register_activation_hook( __FILE__, function () {
add_option( ACME_NOTICE_OPTION, '', '', false );
} );
// Output: always escape.
add_shortcode( 'acme_notice', function () {
$text = get_option( ACME_NOTICE_OPTION, '' );
return $text ? '<p class="acme-notice">' . esc_html( $text ) . '</p>' : '';
} );
// Input: check permissions, then sanitise.
add_action( 'rest_api_init', function () {
register_rest_route( 'acme-notice/v1', '/text', array(
'methods' => 'POST',
'permission_callback' => function () {
return current_user_can( 'edit_pages' );
},
'args' => array(
'text' => array(
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
'callback' => function ( WP_REST_Request $request ) {
update_option( ACME_NOTICE_OPTION, $request['text'], false );
return rest_ensure_response( array( 'saved' => true ) );
},
) );
} );Save it as wp-content/plugins/acme-notice/acme-notice.php, activate it and place [acme_notice] in a page. A logged-in editor can update the text with a POST request to /wp-json/acme-notice/v1/text; when calling it from JavaScript in the admin, send the X-WP-Nonce header created with wp_create_nonce('wp_rest'). Swap the acme_ prefix for your own so names never collide with other plugins.
Use hooks, not hacks
Everything in WordPress extensibility runs on hooks. Actions let you run code at a point in the request, and filters let you modify a value before it is used. The rules that matter in practice:
- Attach code to the earliest sensible hook, and no earlier. Admin code should not load on the front end.
- Never edit core, theme or other plugin files. If a hook does not exist, wrap the behaviour or ask the author for one.
- Use activation hooks for one-time setup such as creating tables, but do not rely on them for upgrades. Store a version number and run migrations on load.
- Expose your own hooks so other developers can extend the plugin without forking it.
- Keep callbacks light. Defer heavy work to scheduled events or background processing.
Security: sanitise, validate, escape, authorise
Most plugin vulnerabilities are the same four mistakes. The plugin security handbook covers each, and the habits below prevent the bulk of them.
- Validate and sanitise input as early as possible, using the function that matches the data type.
- Escape output as late as possible, with the escaping function for the context: HTML, attribute, URL or JavaScript.
- Check capabilities with current_user_can before any action that changes data. Being logged in is not authorisation.
- Verify nonces on every form and AJAX action to prevent cross-site request forgery.
- Prepare database queries with the wpdb prepare method, never by concatenating user input into SQL.
Also avoid storing secrets in plain text options when you can, limit file uploads to what you need, and never trust data just because it came from your own database.
Expose data with the REST API
If your plugin feeds a block, a dashboard or an external app, register custom routes with the REST API rather than writing ad hoc AJAX handlers. Use your own namespace with a version, such as vendor/v1, declare the accepted arguments with types and sanitise callbacks, and always provide a permission callback. A route with no permission check is public by design, so make that a deliberate choice. Return proper HTTP status codes and error objects so clients can react predictably.
Test, optimise and release
Automated checks catch what code review misses. Run PHP_CodeSniffer with the WordPress ruleset, write PHPUnit tests for business logic using the WordPress plugin unit test setup, and run the Plugin Check tool before distribution. Then look at performance: avoid autoloading large options, cache expensive queries with transients or an object cache, and load assets conditionally.
Finally, test on the oldest PHP and WordPress versions you claim to support, with the page builders your customers use, and with your uninstall routine. The official plugin developer handbook is the reference for submission rules if you plan to publish on WordPress.org.
A few housekeeping habits separate professional plugins from hobby code, and they cost very little when done from the start:
- Version and migrate. Store a plugin version in the database and run idempotent upgrade routines when it changes, so customers can skip releases safely.
- Make it translatable. Wrap user-facing strings in translation functions with one text domain, even if you launch in a single language.
- Fail quietly and log usefully. Catch errors around remote requests, show admin notices for actionable problems, and never expose stack traces to visitors.
- Clean up after yourself. Remove options, tables and scheduled events on uninstall, and do not leave orphaned data on customer sites.
- Document the extension points. A short readme listing hooks, filters and REST routes saves hours for the next developer, which is often you in a year.
When to bring in a developer
A simple shortcode or custom post type is a fine learning project. Anything touching payments, user data, licensing or integrations deserves an experienced hand, because mistakes are expensive. Our WordPress plugin development service covers architecture, security review and maintenance, and AYM Multilingual is a production plugin we build and support ourselves. If your plugin will be sold, read how a licensing system fits in before you design the architecture.
Frequently asked questions
What do I need to build a WordPress plugin?
A local WordPress install, PHP knowledge, and one PHP file with a plugin header. For anything beyond a prototype, add version control, a coding standards checker and automated tests.
Is a custom plugin better than installing an existing one?
When existing plugins do not fit your workflow, or you would need several to approximate it, a purpose-built plugin is usually faster and easier to secure. If one maintained plugin does the job, use it.
How do I keep a plugin secure?
Validate and sanitise input, escape output, check user capabilities, verify nonces and prepare every database query. Run static analysis and review before each release.
Can a custom plugin break during WordPress updates?
Plugins that use public hooks and APIs rarely break. Plugins that edit core files or rely on internal functions do. Testing against upcoming WordPress and PHP versions catches issues early.
Related articles
WordPress & multilingual sites
Localization vs Translation: The Difference and How to Apply It to Your Website
Translation moves text into another language; localization rebuilds the site for someone living in that market. We explain the difference with examples from our own keyword research.
WordPress & multilingual sites
Add a Language to a WordPress Site: A Step-by-Step Guide
You want to add English or another language to your existing WordPress site. The “Site Language” setting is not enough; here is how to do it with the right structure, priorities and SEO steps.
WordPress & multilingual sites
Language Switcher Design: User-Friendly, Accessible and SEO-Safe
Flags or language names? Top-right corner or footer? The principles of a language switcher that visitors can find, use with a keyboard and that search engines understand.
