Skip to content

Drupal API — Render Arrays, URL, Database and Entity API

DodaTech Updated 2026-06-27 10 min read

In this tutorial, you'll learn the essential Drupal APIs for module development: render arrays for building HTML output, the URL class for path generation, the database abstraction layer for queries, the entity API for loading and saving content, and the service container for dependency injection.

What You'll Learn

  • Render arrays: #type, #markup, #theme, #attached, #cache
  • Common render elements: html_tag, link, table, item_list, dropbutton, container, status_messages
  • The URL class: Url::fromRoute, fromUri, fromUserInput, toString, toRenderArray
  • Database abstraction: db_select, db_insert, db_update, db_delete, db_query
  • Dynamic queries: select, fields, condition, range, orderBy, execute
  • Entity API: entityTypeManager, load, loadMultiple, create, save, delete
  • Entity queries: entityQuery, condition, sort, range, pager, execute
  • Service container: \Drupal::service(), ::state(), ::config(), ::currentUser()
  • State API for temporary storage
  • Config API for configuration storage
  • Private and shared tempstore

Why It Matters

Drupal's APIs abstract away the complexity of database operations, URL generation, and HTML rendering. Without them, you would write raw SQL, hardcode URLs, and mix HTML in PHP strings. The APIs ensure security (SQL Injection prevention, XSS filtering), consistency (cache tags, bubbleable metadata), and portability (upgrading Drupal does not break your code). Mastering these APIs is essential for writing production-quality Drupal modules.

Real-World Use

A developer building a custom dashboard needs to display a table of recent content with links to edit each item. Using the Entity API, they load the nodes. Using the render array system, they build a table with proper cache tags. Using the URL class, they generate edit links without hardcoding paths. The result is a page that is cacheable, secure, and survives Drupal core updates. The database abstraction layer ensures the module works on MySQL, PostgreSQL, and SQLite without changes.

Learning Path

flowchart LR
  A[Hooks] --> B[Drupal API]
  B --> C[Render Arrays]
  C --> D[URL Class]
  D --> E[Database API]
  E --> F[Entity API]
  F --> G[Service Container]
  G --> H[Drush and Config Management]

Render Arrays

Render arrays are the standard way to build HTML output in Drupal. They are nested arrays that describe what to render, not raw HTML.

Basic Markup

<?php
// Simple text output
$build = [
  '#markup' => '<p>Hello, world!</p>',
];

// Multiple items
$build = [
  'greeting' => [
    '#markup' => '<h1>Welcome</h1>',
  ],
  'content' => [
    '#markup' => '<p>This is the dashboard page.</p>',
  ],
  '#prefix' => '<div class="dashboard">',
  '#suffix' => '</div>',
];

Common Render Elements

<?php
// Link
$build['link'] = [
  '#type' => 'link',
  '#title' => 'Edit article',
  '#url' => Url::fromRoute('node.edit', ['node' => 123]),
];

// Item list (ul/ol)
$build['list'] = [
  '#type' => 'item_list',
  '#items' => ['Task 1', 'Task 2', 'Task 3'],
  '#list_type' => 'ul',
];

// Table
$build['table'] = [
  '#type' => 'table',
  '#header' => ['Title', 'Status', 'Author'],
  '#rows' => [
    ['My Article', 'Published', 'Admin'],
    ['Another Post', 'Draft', 'Editor'],
  ],
  '#empty' => t('No content found.'),
];

// Dropbutton
$build['actions'] = [
  '#type' => 'dropbutton',
  '#links' => [
    'edit' => [
      'title' => 'Edit',
      'url' => Url::fromRoute('node.edit', ['node' => 123]),
    ],
    'delete' => [
      'title' => 'Delete',
      'url' => Url::fromRoute('node.delete', ['node' => 123]),
    ],
  ],
];

// Container (wrapper div)
$build['wrapper'] = [
  '#type' => 'container',
  '#attributes' => ['class' => ['highlighted']],
  'child' => [
    '#markup' => '<p>Inside a container.</p>',
  ],
];

// Status messages
\Drupal::messenger()->addMessage('Content saved successfully.');
\Drupal::messenger()->addWarning('Check your configuration.');
\Drupal::messenger()->addError('An error occurred.');

Attaching Assets

<?php
$build['page'] = [
  '#markup' => '<div id="dashboard"></div>',
  '#attached' => [
    'library' => [
      'my_module/dashboard',
    ],
    'drupalSettings' => [
      'myModule' => [
        'endpoint' => '/api/dashboard',
      ],
    ],
  ],
];

Cache Metadata

<?php
$build['content'] = [
  '#theme' => 'node',
  '#node' => $node,
  '#cache' => [
    'tags' => ['node:' . $node->id()],
    'contexts' => ['user'],
    'max-age' => 3600,
  ],
];

URL Class

Use the Drupal URL class to generate paths safely:

<?php
use Drupal\Core\Url;

// From a route name (preferred)
$url = Url::fromRoute('entity.node.canonical', ['node' => 123]);
$link = $url->toString(); // /node/123

// From an external URI
$url = Url::fromUri('https://example.com');
$link = $url->toString();

// From an internal URI
$url = Url::fromUri('internal:/node/123');
$link = $url->toString();

// From user input (sanitized)
$url = Url::fromUserInput('/node/123');
$link = $url->toString();

// With options
$url = Url::fromRoute('entity.node.canonical', ['node' => 123], [
  'absolute' => true,
  'fragment' => 'comments',
  'query' => ['page' => 2],
]);
$link = $url->toString(); // https://example.com/node/123?page=2#comments

// Render as a link
$build = [
  '#type' => 'link',
  '#title' => 'Read more',
  '#url' => Url::fromRoute('entity.node.canonical', ['node' => 123]),
];

// Access checking
$url = Url::fromRoute('entity.node.edit_form', ['node' => 123]);
if ($url->access()) {
  // Current user can access this route
}

Database Abstraction Layer

Drupal uses a database abstraction layer that works with MySQL, PostgreSQL, SQLite, and others:

<?php
use Drupal\Core\Database\Database;

// Get the database connection
$connection = Database::getConnection();
// Or: $connection = \Drupal::database();

Select Queries

<?php
// Simple select
$query = \Drupal::database()->select('node_field_data', 'n');
$query->fields('n', ['nid', 'title', 'created']);
$query->condition('n.status', 1);
$query->range(0, 10);
$query->orderBy('n.created', 'DESC');
$result = $query->execute()->fetchAll();

foreach ($result as $row) {
  // $row->nid, $row->title, $row->created
}

// Select with join
$query = \Drupal::database()->select('node_field_data', 'n');
$query->fields('n', ['nid', 'title']);
$query->join('users_field_data', 'u', 'n.uid = u.uid');
$query->addField('u', 'name', 'author');
$query->condition('n.type', 'article');
$query->condition('n.status', 1);
$result = $query->execute()->fetchAll();

// Count query
$count_query = \Drupal::database()->select('node_field_data', 'n');
$count_query->addExpression('COUNT(*)', 'count');
$count_query->condition('n.status', 1);
$total = $count_query->execute()->fetchField();

Insert Queries

<?php
// Insert a record
\Drupal::database()->insert('my_table')
  ->fields([
    'title' => 'New Task',
    'status' => 'pending',
    'user_id' => 1,
    'created' => \Drupal::time()->getRequestTime(),
  ])
  ->execute();

// Insert multiple records
$records = [
  ['title' => 'Task 1', 'status' => 'pending', 'user_id' => 1],
  ['title' => 'Task 2', 'status' => 'done', 'user_id' => 1],
];
$query = \Drupal::database()->insert('my_table')
  ->fields(['title', 'status', 'user_id']);
foreach ($records as $record) {
  $query->values($record);
}
$query->execute();

Update and Delete Queries

<?php
// Update records
\Drupal::database()->update('my_table')
  ->fields(['status' => 'completed'])
  ->condition('id', 123)
  ->execute();

// Delete records
\Drupal::database()->delete('my_table')
  ->condition('status', 'archived')
  ->condition('created', strtotime('-1 year'), '<')
  ->execute();

Entity API

The Entity API is the recommended way to work with content, users, taxonomy, and other entities:

<?php
use Drupal\node\Entity\Node;
use Drupal\user\Entity\User;

// Loading a single node
$node = Node::load(123);
$title = $node->getTitle();
$created = $node->getCreatedTime();
$author = $node->getOwner()->getDisplayName();

// Loading multiple nodes
$nodes = Node::loadMultiple([123, 124, 125]);

// Using entityTypeManager
$storage = \Drupal::entityTypeManager()->getStorage('node');
$node = $storage->load(123);
$nodes = $storage->loadMultiple([123, 124]);
$nodes = $storage->loadByProperties(['type' => 'article', 'status' => 1]);

// Creating a new node
$node = Node::create([
  'type' => 'article',
  'title' => 'My New Article',
  'body' => 'This is the article body.',
  'status' => 1,
  'uid' => 1,
]);
$node->save();

// Updating a node
$node = Node::load(123);
$node->setTitle('Updated Title');
$node->set('field_custom', 'New value');
$node->save();

// Deleting a node
$node = Node::load(123);
$node->delete();

Entity Queries

For complex queries, use entity queries instead of direct database queries:

<?php
use Drupal\Core\Entity\EntityInterface;

// Basic entity query
$query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->condition('status', 1)
  ->condition('title', 'security', 'CONTAINS')
  ->sort('created', 'DESC')
  ->range(0, 10)
  ->accessCheck(TRUE);

$nids = $query->execute();

// Load the results
$nodes = Node::loadMultiple($nids);

// Query with fields
$query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->condition('field_category', 5)
  ->condition('field_tags', [1, 2, 3], 'IN')
  ->sort('title', 'ASC')
  ->pager(20);

$nids = $query->execute();

// Count query
$count_query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->condition('status', 1)
  ->accessCheck(FALSE);
$total = $count_query->count()->execute();

// Entity query for users
$uids = \Drupal::entityQuery('user')
  ->condition('status', 1)
  ->condition('roles', 'content_editor')
  ->sort('created', 'DESC')
  ->execute();

Service Container

Drupal uses dependency injection through the service container:

<?php
// Accessing services statically (for quick scripts)
$service = \Drupal::service('my_module.api_client');
$config = \Drupal::config('system.site');
$state = \Drupal::state();
$current_user = \Drupal::currentUser();
$entity_type_manager = \Drupal::entityTypeManager();
$messenger = \Drupal::messenger();
$logger = \Drupal::logger('my_module');

// Proper dependency injection in a controller
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

class DashboardController extends ControllerBase {
  protected $apiClient;

  public function __construct($api_client) {
    $this->apiClient = $api_client;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('my_module.api_client')
    );
  }

  public function page() {
    $data = $this->apiClient->fetchData();
    return [
      '#theme' => 'dashboard',
      '#data' => $data,
    ];
  }
}

State API

The State API stores temporary, non-configuration data:

<?php
$state = \Drupal::state();

// Store a value
$state->set('my_module.last_cron_run', time());
$state->set('my_module.api_threshold', 1000);

// Retrieve a value
$last_run = $state->get('my_module.last_cron_run');
$threshold = $state->get('my_module.api_threshold', 500);

// Check if a key exists
if ($state->get('my_module.setup_completed')) {
  // Setup has been done
}

// Delete a key
$state->delete('my_module.temp_data');

// Get multiple keys at once
$values = $state->getMultiple([
  'my_module.last_cron_run',
  'my_module.api_threshold',
]);

Config API

The Config API stores configuration data:

<?php
// Reading configuration
$config = \Drupal::config('system.site');
$site_name = $config->get('name');
$site_slogan = $config->get('slogan');

// Reading module configuration
$config = \Drupal::config('my_module.settings');
$api_endpoint = $config->get('api_endpoint');
$sync_interval = $config->get('sync_interval');

// Writing configuration (use getEditable)
\Drupal::configFactory()
  ->getEditable('my_module.settings')
  ->set('api_endpoint', 'https://new-api.example.com')
  ->set('sync_interval', 120)
  ->save();

Tempstore

Tempstore provides temporary storage for user sessions:

<?php
// Private tempstore (per user)
$tempstore = \Drupal::service('tempstore.private')
  ->get('my_module');
$tempstore->set('wizard_step', 3);
$step = $tempstore->get('wizard_step');
$tempstore->delete('wizard_step');

// Shared tempstore (across users)
$shared_tempstore = \Drupal::service('tempstore.shared')
  ->get('my_module');
$shared_tempstore->set('current_batch', 42);

Common Mistakes

  1. Using raw SQL instead of entity queries: Entity queries are portable, cacheable, and respect access control. Raw SQL bypasses all of this. Use entity queries for entity data and the database abstraction layer only for custom non-entity tables.

  2. Returning HTML strings instead of render arrays: Render arrays allow Drupal to apply cache metadata, bubble contexts, and attach assets. Returning <p>Hello</p> directly breaks Caching and asset management.

  3. Hardcoding URLs: Use Url::fromRoute() instead of hardcoding /node/123. Routes can change between versions, but route names stay the same.

  4. Forgetting accessCheck() on entity queries: Drupal 10+ requires explicit accessCheck(TRUE) or accessCheck(FALSE). Omitting it throws an exception.

  5. Not using dependency injection: Static calls to \Drupal::service() work but make testing impossible. Inject dependencies via the constructor for testable, maintainable code.

Practice Questions

  1. How would you build a render array for a table of the last 10 published articles with columns for title, author, and date?
  2. What is the difference between \Drupal::state()->set() and \Drupal::configFactory()->getEditable()->set()?
  3. Why must you use accessCheck() on entity queries, and what happens if you omit it?
  4. Challenge: Build a custom dashboard page that: renders a table of the 5 most recent nodes (any type) using render arrays, generates proper edit links using Url::fromRoute(), includes a counter of total published nodes using an entity query, attaches a custom CSS library, sets proper cache tags for each node displayed, and uses dependency injection for the entity type manager.

FAQ

What is a render array in Drupal?

A render array is a structured PHP array that describes HTML output without writing raw HTML. It uses keys like #type, #markup, #theme, #attached, and #cache. Drupal's rendering system converts these arrays into HTML, applying caching, theming, and asset management automatically.

Should I use Entity API or the database abstraction layer?

Use the Entity API for entities (nodes, users, taxonomy terms). Use the database abstraction layer only for custom, non-entity tables (logging, custom storage). Entity queries are portable, cacheable, and respect access control; raw database queries do not.

How do I access configuration values in a controller?

Inject the config factory service using dependency injection, or use \Drupal::config('my_module.settings')->get('key') for static access. The config factory gives you typed access with default values and schema validation.

What is the difference between State API and Config API?

State API stores temporary data (last cron run, cache timestamps) that does not need to survive configuration export/import. Config API stores permanent configuration (API endpoints, site name) that should be exported, version-controlled, and deployed across environments.

How do I attach CSS to a page via render array?

Add '#attached' => ['library' => ['my_module/my_library']] to your render array. Define the library in your module's .libraries.yml file with the CSS path. Drupal automatically includes the CSS when rendering that element.

Mini Project

Goal: Build a custom module that uses multiple Drupal APIs.

  1. Create a module called content_dashboard with a controller at /dashboard
  2. Build a render array with an HTML title, a table of recent content, and a dropbutton with actions
  3. Use the Entity API to load the 10 most recently updated nodes
  4. Use the URL class to generate edit and delete links for each node
  5. Use the Config API to read the site name and display it in the page header
  6. Use the State API to track how many times the dashboard has been viewed
  7. Attach a custom CSS library to style the dashboard
  8. Set cache tags on the render array so it invalidates when any node changes
  9. Inject dependencies via create() instead of using static calls

What's Next

Now that you master Drupal's APIs, proceed to Drush commands to manage Drupal from the command line. Then explore configuration management to learn how to deploy configuration across environments.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro