Back to Blog
Directus: When a Headless CMS Becomes Your Entire Backend
8 min readJul 16, 20267 views

Directus: When a Headless CMS Becomes Your Entire Backend

We often think of headless CMS platforms as just content repositories. But what if one could handle your data, authentication, file storage, and even custom business logic, becoming a true backend replacement? Directus offers a compelling vision for this.

BackendFull-StackSoftware DesignDatabaseTooling
Share

by Sunil Band

Rethinking the Backend: Beyond CRUD and Microservices

For years, we've had this tension: either roll your own API from scratch, dealing with databases, auth, and file uploads, or embrace a full-stack framework with all its opinions. Then came the microservice craze, promising flexibility but often delivering operational complexity. Meanwhile, many projects just need a solid data layer, access control, and a way to manage assets.

Headless CMS solutions like Strapi or Sanity stepped in to solve the content problem, providing APIs for structured data. But they often stop there. What if you need more than just content? What if your "content" is really just application data, and you want full control over your database, fine-grained permissions, and even custom server-side business logic, all without writing boilerplate?

This is where Directus shines. It's not just a headless CMS; it's an open-source data platform that wraps your SQL database with a powerful API and an intuitive admin interface. It flips the script: instead of defining your schema in code and then migrating it to the database, you define your schema directly in the database, and Directus automatically generates everything else.

The Directus Philosophy: Your Database, Your Rules

The core idea behind Directus is simple yet profound: your database is the source of truth. Directus doesn't abstract away your database; it enhances it. You can connect it to an existing SQL database (Postgres, MySQL, SQLite, Oracle, MS SQL Server) or let it provision a new one. Once connected, it introspects your schema and instantly provides:

  1. A REST and GraphQL API: Fully featured, real-time, and automatically reflecting any changes to your database schema.
  2. An intuitive Admin App: A no-code interface to manage your data, users, roles, permissions, and even translations.
  3. Powerful Extensions: Custom hooks, endpoints, and modules for when you need to go beyond standard CRUD operations.

This approach means you're never locked in. If you ever decide to move away from Directus, your data is in a standard SQL database, perfectly accessible. You're not tied to a proprietary data model or storage mechanism, which is a huge win for long-term project viability.

Getting Started: Bootstrapping a Full Backend

Let's walk through setting up Directus with a Postgres database. I typically use Docker Compose for local development because it's clean and reproducible.

First, create a docker-compose.yml file:

yaml
version: '3.8'
services:
  directus:
    image: directus/directus:10.11.0 # Pin to a specific version for stability
    ports:
      - "8055:8055"
    environment:
      KEY: "super-secret-key-that-you-should-change-in-production" # Essential for security
      SECRET: "another-super-secret-that-you-should-change-in-production" # Also essential
      DB_CLIENT: "pg"
      DB_HOST: "db"
      DB_PORT: "5432"
      DB_DATABASE: "directus"
      DB_USER: "directus"
      DB_PASSWORD: "directus"
      ADMIN_EMAIL: "admin@example.com"
      ADMIN_PASSWORD: "password" # Change this IMMEDIATELY in production!
    volumes:
      - ./uploads:/directus/uploads # Persist uploaded files
    depends_on:
      - db
  db:
    image: postgres:15-alpine # Lightweight Postgres image
    environment:
      POSTGRES_DB: "directus"
      POSTGRES_USER: "directus"
      POSTGRES_PASSWORD: "directus"
    volumes:
      - ./data:/var/lib/postgresql/data # Persist database data

Run docker-compose up -d to spin up both the Postgres database and Directus. Once it's running, navigate to http://localhost:8055 in your browser. You'll be greeted by the Directus admin interface, pre-populated with the admin user defined in your docker-compose.yml.

Defining Your Data Model

Inside the admin app, you'll find the Data Studio. This is where you define your collections (tables) and fields (columns). Let's say we're building a simple blog. We'd create a Posts collection:

  1. Click the + icon next to "Collections".
  2. Name it Posts.
  3. Add fields like title (string), slug (string, unique), content (markdown), status (dropdown with 'draft', 'published', 'archived'), published_on (datetime), and author (many-to-one relationship to a Users collection).

As you define these fields and relationships in the Directus UI, it's modifying your underlying Postgres database schema. This is powerful: your data model is visually managed but lives in a standard SQL database. No ORM migrations, no code generation steps. It's just... your database.

Instant APIs: REST and GraphQL

Once your Posts collection is defined, Directus instantly provides APIs for it. You can fetch all posts, a single post, create new posts, update, and delete them, all out of the box. No manual API endpoint creation needed. For example, to fetch all published posts:

typescript
import { Directus } from '@directus/sdk';

// Initialize the SDK
const directus = new Directus<any>('http://localhost:8055'); // You'd type this properly in a real app

async function getPublishedPosts() {
  try {
    const response = await directus.items('posts').readByQuery({
      filter: {
        status: { _eq: 'published' }
      },
      fields: ['id', 'title', 'slug', 'content', 'published_on.UTC', 'author.first_name'], // Select specific fields, including nested ones
      sort: ['-published_on']
    });

    if (response.data) {
      console.log('Published Posts:', response.data);
      return response.data;
    } else if (response.errors) {
      console.error('API Errors:', response.errors);
      throw new Error('Failed to fetch posts');
    }
  } catch (error) {
    console.error('Network or other error:', error);
    throw error;
  }
}

getPublishedPosts();

The Directus SDK simplifies interacting with these APIs, but you can also use plain fetch or axios. The key is that the API automatically supports filtering, sorting, pagination, and nested relational data, all via query parameters (for REST) or GraphQL queries.

Authentication and Authorization

Directus provides robust authentication out of the box, supporting email/password, OAuth providers (Google, GitHub, etc.), and even JWTs for programmatic access. Once users are authenticated, you can define granular role-based access control (RBAC).

For example, you can create a 'Public' role that can only read published posts, an 'Author' role that can create and edit their own posts (but not delete), and an 'Admin' role with full access. This level of control, configured purely through the UI, saves an immense amount of development time.

Beyond CRUD: Custom Logic with Hooks and Endpoints

While Directus excels at data management, real applications often need custom business logic. This is where Hooks and Custom Endpoints come in.

Hooks allow you to trigger custom server-side JavaScript (or TypeScript) code before or after any data operation. Need to send a notification when a post is published? Or validate complex business rules before saving an item? Hooks are your answer.

Here's a simple example of a hook that logs a message when a new post is created:

typescript
// In a custom hook file, e.g., src/extensions/hooks/post-logger/index.js

module.exports = function registerHook({
  action,
  logger
}) {
  action('items.create.posts', ({
    item,
    collection
  }) => {
    logger.info(`New item created in "${collection}" with ID: ${item.id} and title: "${item.title}"`);
    // You could also trigger an external API call, send an email, etc.
  });
};

Custom Endpoints let you define entirely new API routes that execute custom logic. This is perfect for complex operations that don't map directly to CRUD, like a custom search algorithm, generating a report, or integrating with a third-party service. You define the route, the HTTP method, and the handler function, all within the Directus extension framework.

typescript
// In a custom endpoint file, e.g., src/extensions/endpoints/custom-reports/index.js

module.exports = function registerEndpoint({
  router,
  services,
  exceptions
}) {
  const { PostsService } = services;
  const { InvalidPayloadException } = exceptions;

  router.get('/custom-reports/published-count', async (req, res) => {
    try {
      const postsService = new PostsService({
        schema: req.schema,
        accountability: req.accountability
      });
      const publishedPosts = await postsService.readByQuery({
        filter: {
          status: {
            _eq: 'published'
          }
        }
      });
      res.json({
        count: publishedPosts.length
      });
    } catch (error) {
      throw new InvalidPayloadException(error.message);
    }
  });
};

These extensions blur the line between a "headless CMS" and a full-fledged application backend. You get the speed of configuration for common tasks and the flexibility of code for unique requirements.

Trade-offs and Considerations

No tool is a silver bullet, and Directus has its own set of trade-offs:

  • Performance at Scale: While Directus itself is performant, its automatic API generation might not always be as optimized as a hand-tuned, custom API for extremely high-volume, complex queries. For most applications, it's more than sufficient, but for Pinterest-scale traffic, you might hit limits on how much you can optimize through configuration.
  • Overhead for Simple Projects: For a trivial project with only one or two data tables, setting up Directus might feel like overkill. A simple Node.js script with an ORM might be faster to get off the ground. However, as soon as you add authentication, file storage, or more complex data types, Directus quickly justifies its setup.
  • Learning Curve for Advanced Customization: While the basic usage is intuitive, diving into custom hooks, endpoints, and event flows requires understanding the Directus extension API and its lifecycle. It's well-documented, but it's still code you need to write and maintain.
  • Database Schema Management: Directus modifies your database schema directly via its UI. This is a strength for rapid development but can be a challenge in a heavily regulated environment or a large team where database migrations are tightly controlled via code. It's important to have a strategy for managing schema changes across environments (e.g., using a schema migration tool alongside Directus, or treating Directus as the primary schema manager).

Despite these points, for a vast number of applications, especially internal tools, content-heavy sites, and even SaaS products, Directus provides an incredibly efficient path to a robust backend.

Wrapping up

Directus challenges the traditional view of what a headless CMS can be. It's a powerful, open-source platform that gives you total control over your data while abstracting away the tedious parts of API development, authentication, and content management. If you're tired of writing boilerplate for every new project, or if you want to empower non-developers to manage complex data without giving them direct database access, Directus is absolutely worth exploring.

My recommendation? Spin up the Docker Compose example, connect it to your database of choice, and spend an hour building out a complex data model with relationships, file uploads, and custom roles. You'll be surprised at how quickly you can build a fully functional backend API and admin panel without touching a single line of backend code.

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕