Building a form class extending FormBasefor Drupal 11 , 10 , 9 , and 8

Last updated :  

Extending FormBase is like inheriting a fully-equipped workshop instead of building your tools from scratch — translation helpers, messenger service access, current-user lookups, and the entire Form API contract all come free the moment your class extends it. This lesson builds one, piece by piece, from an empty class to a working form.

What you'll learn in this lesson

  • What extending FormBase actually gives you for free
  • The minimum viable form class that Drupal will accept
  • Registering the form so a route can actually reach it

The minimum viable class

namespace Drupal\mymodule\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class ContactForm extends FormBase {

  public function getFormId() {
    return 'mymodule_contact_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
  }
}

This compiles and runs — an empty form with just the invisible CSRF/build-tracking fields Drupal always adds. Every real form starts from exactly this shape before fields get added.

What FormBase gives you for free

  • $this->t() — the translation helper, via StringTranslationTrait, without injecting the translation service manually.
  • $this->messenger() — status/error message helper, via MessengerTrait.
  • $this->currentUser() — the logged-in user, without injecting the current_user service manually.
  • $this->config() — shorthand for reading configuration objects.
Why this matters: a form class that instead directly implements FormInterface (the interface FormBase itself implements) gets none of this — it has to declare its own create() factory and manually inject every service it needs, the more verbose, fully-explicit approach you'll see in a later lesson. FormBase is the practical default for the vast majority of forms; the bare interface is for cases needing full explicit control.

Registering the route

# mymodule.routing.yml
mymodule.contact_form:
  path: '/contact-form-example'
  defaults:
    _form: '\Drupal\mymodule\Form\ContactForm'
    _title: 'Contact us'
  requirements:
    _permission: 'access content'

The _form route default (rather than _controller) tells Drupal's routing system this route resolves directly to a Form API class — Drupal handles instantiating it and calling buildForm() automatically, no controller wrapper code needed.

Quick check: your form class needs to inject a custom service that isn't one of FormBase's built-in helpers (translation, messenger, current user, config). Does extending FormBase prevent you from doing that? If you said no — you still implement create() and a constructor to inject additional services, FormBase just means you don't have to re-implement the common ones — you've understood that FormBase adds convenience without removing flexibility.

Key takeaways

  • FormBase provides translation, messenger, current-user, and config helpers automatically via traits.
  • The minimum viable form class needs only getFormId(), buildForm(), and submitForm() — even empty implementations satisfy the contract.
  • A route resolves to a form class via the _form default, not _controller.
  • Extending FormBase doesn't prevent injecting additional custom services — it just covers the common ones for free.

Coming up next

You've got an empty form registered and reachable. Next, we fill in buildForm() with actual fields.