YAML File Standards
Sooner or later, every Drupal project means writing YAML — module info files, routes, services, libraries, configuration. YAML looks forgiving, but its indentation rules are not, and a single misplaced space is enough to make Drupal fail to parse a file at all.
1. Indentation
YAML uses 2 spaces per indentation level, never tabs — a tab character in a YAML file is a parse error waiting to happen, and Drupal's error message for it rarely points at the actual line.
# ❌ Incorrect: no indentation, the nested structure can't be parsed.
product_import.settings:
path: '/admin/config/product-import'
defaults:
_title: 'Product Import'
# ✅ Correct: 2-space indentation shows the nested structure.
product_import.settings:
path: '/admin/config/product-import'
defaults:
_title: 'Product Import'2. Common YAML Files
A handful of YAML file types show up in nearly every module: .info.yml declares the module itself, .routing.yml defines its routes, .services.yml its services, .libraries.yml its CSS/JS assets, and .permissions.yml its permissions. Each one follows the same 2-space indentation rule, and each has its own well-documented top-level structure.
name: 'Product Import'
type: module
description: 'Imports products from the external catalog API.'
package: Custom
core_version_requirement: ^10 || ^11
dependencies:
- drupal:node3. Routing Definitions
A route name is prefixed with the module's machine name, same as everything else. _controller points to the class and method that handles the request; _title sets the page title; requirements controls who's allowed to reach it.
product_import.status_page:
path: '/admin/reports/product-import'
defaults:
_controller: '\Drupal\product_import\Controller\ImportController::status'
_title: 'Import status'
requirements:
_permission: 'administer product import'4. Service Definitions
Service YAML files follow the pattern covered in the architecture chapter: a namespaced service ID, the class it instantiates, and its constructor arguments as other service IDs prefixed with @. Booleans in a service definition — like shared: false — are lowercase, unlike the TRUE/FALSE used in PHP.
services:
product_import.importer:
class: Drupal\product_import\ProductImporter
arguments: ['@logger.factory', '@http_client']5. Config Schema
Any module that ships configuration — a settings form backed by \Drupal::configFactory(), for instance — needs a matching schema file under config/schema/, describing the type of every key the configuration can contain. Skipping this doesn't break anything obviously at first, but it silently breaks configuration translation, validation, and the config import/export workflow.
product_import.settings:
type: config_object
label: 'Product Import settings'
mapping:
api_endpoint:
type: string
label: 'API endpoint URL'
batch_size:
type: integer
label: 'Batch size'6. Quoting and Booleans
A plain unquoted string is fine for most values, but anything that could otherwise be parsed as a number, a boolean, or that starts with a special character gets wrapped in single quotes to keep YAML from misreading it. Double quotes are reserved for strings that need escape sequences.
# ❌ Incorrect: unquoted values that YAML will misinterpret.
version: 2.0
enabled: yes
path: /admin/config
# ✅ Correct: quoted where it actually matters.
version: '2.0'
enabled: true
path: '/admin/config'