How do you build secure, forward-compatible Drupal modules?
Drupal Developer
answer
Custom Drupal module development requires strict adherence to standards and forward compatibility. Follow Drupal Coding Standards (PSR-4, Drupal Rector), use APIs/hooks/services over hacks, and document everything with inline comments and README files. Apply security best practices: validate/sanitize input, escape output, respect permissions, and avoid direct SQL (use Database API/Entity API). Test with PHPUnit/Kernel tests, keep modules upgrade-safe, and run phpcs with Drupal standards before merging.
Long Answer
Developing custom modules in Drupal is one of the most powerful ways to extend functionality, but it comes with responsibilities: ensuring forward compatibility, maintaining security, and upholding coding standards. A mature approach blends architecture, security practices, testing, and governance.
1. Follow Drupal’s APIs and standards
Avoid “hacks” like altering core code. Use the defined APIs, service containers, events, and hooks. This ensures your module remains compatible with future Drupal core releases. Namespaces should follow PSR-4 autoloading; code should be linted with Drupal Coder rules for phpcs. Document changes in README, CHANGELOG.txt, and inline PHPDoc.
2. Forward compatibility
Core evolves, so write with flexibility:
- Use dependency injection, not global \Drupal:: calls.
- Use configuration entities rather than hard-coded settings.
- Avoid deprecated APIs; track deprecations with Drupal Rector.
- Keep logic modular; services can be swapped when APIs change.
- Support semantic versioning, defining info.yml properly with core requirements.
3. Security practices
Security is non-negotiable:
- Always sanitize and validate user input with \Drupal\Component\Utility.
- Escape output with Html::escape or Twig auto-escape.
- Enforce access checks using Drupal’s AccessResult and permission API.
- Use Database API/Entity API rather than raw SQL.
- Limit file uploads with MIME/type validation; rely on Drupal’s File API.
- Respect CSRF protection using form API tokens.
- Test against OWASP Top 10 threats (XSS, SQL injection, CSRF, privilege escalation).
4. Coding standards and quality
Modules should look like core: follow Drupal’s coding standards and style guidelines. Tools:
- phpcs with Drupal Coder for linting.
- Static analysis with phpstan-drupal.
- Automated code reviews via CI/CD pipelines.
- Unit, Kernel, and Functional tests with PHPUnit and Behat.
5. Config and schema management
Every setting must live in Drupal’s configuration management system. Schema definitions should exist for new configuration keys. This ensures consistency between environments and supports automated deployment.
6. Documentation and maintainability
Provide clear README files, usage instructions, and change logs. Annotate plugins, controllers, and services. Define routes in YAML with descriptive comments. Avoid “magic numbers” or hidden config—everything should be discoverable.
7. Performance and scalability
Use Drupal’s cache API (cache tags, cache contexts, cache max-age) correctly. Avoid expensive queries inside hooks. Leverage lazy builders and render caching for frontend output. Performance-conscious coding ensures modules scale with enterprise traffic.
8. CI/CD and testing
Run automated pipelines:
- Unit + Kernel tests per merge.
- Static analysis for deprecations.
- Security scans (e.g., PHPStan security rules).
- Automated deployment to staging for QA.
9. Governance and lifecycle
Modules should have owners, release notes, and a versioning policy. Track dependencies explicitly in composer.json. Participate in Drupal Security Advisories when releasing public modules.
By following these practices, a custom Drupal module becomes not only a business enabler but a stable, secure, and maintainable piece of the enterprise stack.
Table
Common Mistakes
Frequent mistakes include bypassing APIs with raw SQL, leading to fragile code and security risks. Hardcoding logic instead of using config entities creates deployment drift. Relying on \Drupal:: global calls rather than dependency injection makes modules hard to maintain. Ignoring deprecation notices means sudden breakage after core upgrades. Developers often skip input/output sanitization, leaving room for XSS or SQL injection. Others ignore access checks, assuming UI-level protection is enough. Missing schema definitions break configuration sync. Another trap: storing business logic in hooks instead of services, leading to performance bottlenecks. Teams sometimes skip automated tests, causing regressions during Drupal updates. Finally, failing to run phpcs with Drupal Coder results in inconsistent code quality and technical debt.
Sample Answers (Junior / Mid / Senior)
Junior:
“I follow Drupal’s hooks and APIs instead of hacking core. I use Database API for queries, and validate inputs with Drupal utilities. I run phpcs with Drupal Coder to align with standards.”
Mid:
“I build modules using services and dependency injection for maintainability. Config entities hold settings, and schema definitions ensure portability. I secure modules by validating inputs, enforcing access checks, and testing with PHPUnit.”
Senior:
“My approach is architectural: modules align with Drupal’s service container, cache API, and config management. I enforce coding standards with CI/CD pipelines, run phpstan-drupal to catch deprecations, and use Rector for forward compatibility. Security is layered: sanitized input, escaped output, CSRF tokens, and permission checks. We maintain semantic versioning and changelogs for governance.”
Evaluation Criteria
Interviewers look for deep familiarity with Drupal’s architecture and coding standards. Strong answers mention PSR-4 autoloading, dependency injection, service container, hooks, and events. They should cover security best practices: input/output sanitization, escaping in Twig, CSRF tokens, permission checks, and Database API. Forward compatibility is shown by avoiding deprecated APIs, using Rector, and tracking deprecations in CI/CD. Strong candidates emphasize automated testing (unit, kernel, functional), static analysis, and phpcs linting. They also highlight performance with cache API usage and governance practices like composer.json dependency management and semantic versioning. Weak answers focus only on “writing code that works” without mentioning standards, testing, or future-proofing.
Preparation Tips
To prepare, set up a demo module: create a config entity with schema, define routes, and build a simple controller with dependency injection. Add access checks, sanitize inputs, and escape outputs in Twig. Test with PHPUnit (unit + kernel). Run phpcs with Drupal Coder and phpstan-drupal to catch issues. Add a README and changelog. Use Rector to refactor deprecated APIs. Practice explaining cache tags, contexts, and max-age in performance tuning. Simulate an upgrade from Drupal 9 to 10, ensuring the module still works. Review Drupal Security advisories and OWASP Top 10 to connect your module practices to real-world risks. Document and version the module in composer.json, then publish it to Packagist or a private repo to practice full lifecycle management.
Real-world Context
A government portal needed a custom workflow module. Initial builds bypassed APIs with raw SQL, breaking during a Drupal upgrade. Rewriting with services, Database API, and config entities fixed compatibility. A media company used Drupal Rector to future-proof modules across core versions. In e-commerce, weak access checks allowed unintended users to view product drafts; switching to AccessResult::forbiddenIf solved the issue. A healthcare site faced data leakage from unescaped Twig variables—after adding auto-escape and Html::escape, vulnerabilities dropped. CI/CD pipelines with phpcs + phpstan-drupal caught regressions before deployment. Organizations that invested in coding standards and automated tests reduced upgrade time by 60%. Across industries, the consistent theme: modules built with APIs, security best practices, and governance outlast projects and scale reliably.
Key Takeaways
- Always follow Drupal APIs and PSR-4 coding standards.
- Use dependency injection, not global \Drupal::.
- Secure with sanitization, escaping, CSRF, and permission checks.
- Define config schemas; rely on config management for portability.
- Automate testing, linting, and deprecation tracking.
Practice Exercise
Scenario: You must develop a custom Drupal module that provides a secure content approval workflow.
Tasks:
- Create a config entity to manage workflow stages. Add schema definitions so settings export across environments.
- Build a controller with dependency injection; avoid global calls.
- Add access checks based on user roles and permissions. Validate all user inputs; escape outputs in Twig.
- Store workflow transitions in the Database API, ensuring queries are parameterized and idempotent.
- Implement cache tags/contexts so approved content invalidates caches automatically.
- Write PHPUnit unit tests for business logic and kernel tests for integration. Add Behat tests for UI workflows.
- Run phpcs with Drupal Coder and phpstan-drupal in CI/CD to enforce standards and catch deprecated APIs.
- Document the module with a README, changelog, and composer.json metadata.
- Secure forms with CSRF tokens, file uploads with MIME validation, and respect access results in all routes.
Deliverable: Present a diagram + 60-second pitch where you demonstrate config portability, access checks in action, cache invalidation, and automated tests passing. Show how your Drupal module development approach ensures security, forward compatibility, and coding standard adherence.

