Elixir

Integrate Elixir with Strapi for scalable, high-performance content delivery, optimizing content management and handling high-concurrency applications efficiently

Elixir

What Is Elixir?

Elixir is a dynamic, functional programming language designed for building scalable and maintainable applications. Elixir runs on the Erlang Virtual Machine (BEAM) and uses Erlang’s robust concurrency and fault-tolerance features.

Elixir is ideal for systems that require high availability, real-time processing, and concurrency, making it popular for web development, distributed systems, and telecommunications. Elixir’s syntax is clean and easy to learn, while its built-in support for concurrency allows developers to handle many tasks simultaneously without sacrificing performance. With a strong focus on developer productivity and reliability, Elixir is a powerful choice for modern applications that demand scalability.

Why Integrate Elixir with Strapi

Integrating Elixir with Strapi creates a powerful foundation for modern web applications, combining Strapi's intuitive content management capabilities with Elixir's scalability and fault tolerance. This integration effectively addresses complementary needs: Strapi offers a user-friendly admin panel and flexible APIs, while Elixir excels in high-concurrency environments, making it perfect for content-heavy applications that need to scale.

Elixir’s functional programming approach ensures stable, predictable applications. Elixir’s immutable data structures and actor-based concurrency allow efficient handling of millions of concurrent users, ensuring high availability. When paired with Strapi’s flexible APIs (REST and GraphQL), Elixir’s concurrent processing capabilities shine, allowing multiple API requests to be processed in parallel. This setup is particularly useful for managing complex content relationships and aggregating data from multiple sources.

Strapi’s API flexibility, combined with Elixir’s fault-tolerant architecture, creates a robust system where failures are automatically managed, ensuring content delivery remains uninterrupted. The coexistence of REST and GraphQL APIs in Strapi further enhances performance, offering developers the flexibility to optimize for specific needs.

Elixir’s Phoenix framework outperforms traditional frameworks like Ruby on Rails in high-concurrency scenarios, improving user experience and reducing infrastructure costs. Together, Strapi and Elixir offer a seamless, scalable solution that optimizes both content management and delivery performance.

How to Integrate Elixir with Strapi

Combining Strapi, an open-source Node.js Headless CMS, with your Elixir application gives you modern content management with Elixir's performance and concurrency. Let's walk through setting up a production-ready integration.

Prerequisites and Environment Setup

You'll need Node.js 18+ for Strapi with Yarn or npm. For Elixir, use version 1.15+ with Phoenix 1.7+ for web applications. Set up PostgreSQL or SQLite for your database.

Install HTTPoison for API communication in your Elixir project:

defp deps do
  [
    {:httpoison, "~> 2.0"},
    {:jason, "~> 1.4"}
  ]
end

Configure environment variables for API keys and database credentials from the start. Never hardcode sensitive values.

Creating a Strapi Project

Create a new Strapi project with the CLI:

npx create-strapi-app@latest my-cms-project --quickstart

For production-like development, use Docker Compose:

version: '3'
services:
  strapi:
    image: strapi/strapi
    environment:
      DATABASE_CLIENT: postgres
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_NAME: strapi
      DATABASE_USERNAME: strapi
      DATABASE_PASSWORD: strapi
    volumes:
      - ./app:/srv/app
    ports:
      - '1337:1337'
    depends_on:
      - db

  db:
    image: postgres:13
    environment:
      POSTGRES_DB: strapi
      POSTGRES_USER: strapi
      POSTGRES_PASSWORD: strapi
    volumes:
      - strapi-data:/var/lib/postgresql/data

volumes:
  strapi-data:

Admin Setup and Access Token Creation

Navigate to http://localhost:1337/admin and create your administrator account. Go to Settings > API Tokens > Create new API Token. Use "Full access" for development, but implement Strapi's user roles and permissions for production.

Store the token in your environment:

export STRAPI_API_TOKEN="your_generated_token_here"
export STRAPI_BASE_URL="http://localhost:1337"

Content Modeling in Strapi

Design content types that align with your Elixir application's data structures. Use the Content-Type Builder to create models. For a blog, create an "Article" content type with title, content, author, and publication date fields.

Create reusable components for content that appears across multiple types. Follow API design best practices by grouping related fields and avoiding deeply nested structures.

Connecting Elixir to Strapi APIs

Create a dedicated module to integrate Elixir with Strapi:

defmodule MyApp.StrapiClient do
  @base_url System.get_env("STRAPI_BASE_URL")
  @api_token System.get_env("STRAPI_API_TOKEN")

  def get_articles do
    "/api/articles"
    |> build_url()
    |> HTTPoison.get!(headers())
    |> handle_response()
  end

  def get_article(id) do
    "/api/articles/#{id}"
    |> build_url()
    |> HTTPoison.get!(headers())
    |> handle_response()
  end

  defp build_url(path), do: @base_url <> path

  defp headers do
    [
      {"Authorization", "Bearer #{@api_token}"},
      {"Content-Type", "application/json"}
    ]
  end

  defp handle_response(%HTTPoison.Response{status_code: 200, body: body}) do
    Jason.decode!(body)
  end

  defp handle_response(%HTTPoison.Response{status_code: status_code}) do
    {:error, "API request failed with status #{status_code}"}
  end
end

Build a Complete Elixir-Strapi News Platform

This news platform demonstrates production-ready integration patterns between Phoenix and Strapi. The architecture handles thousands of concurrent readers while maintaining sub-100ms response times through strategic caching and Elixir's actor model. When building such platforms, it's important to consider critical factors, much like when choosing a CMS for e-commerce, to ensure scalability, performance, and maintainability.

Architecture Overview

The platform combines Strapi for Phoenix's headless CMS capabilities with Phoenix's real-time features. Content editors manage articles through Strapi's admin interface while readers experience fast page loads and live content updates.

Core Integration Features:

  • Secure Authentication: JWT-based API communication with token rotation
  • Intelligent Caching: Multi-layer Redis caching that reduces Strapi API calls by 85%
  • Real-time Content: Phoenix Channels broadcast new articles instantly to active readers
  • Fault Tolerance: Circuit breakers and exponential backoff prevent cascade failures
  • Content Preloading: Background processes warm caches before traffic spikes

Critical Implementation Details

The StrapiClient module abstracts all CMS communication, facilitating the integration of Elixir with Strapi:

defmodule NewsApp.StrapiClient do
  @strapi_url System.get_env("STRAPI_URL")
  @jwt System.get_env("STRAPI_JWT")
  
  def fetch_articles do
    HTTPoison.get!(
      "#{@strapi_url}/api/articles?populate=*",
      [{"Authorization", "Bearer #{@jwt}"}],
      recv_timeout: 5_000
    )
    |> handle_response()
    |> cache_articles()
  end
end

JWT authentication implementation with automatic token rotation ensures secure API communication:

defmodule NewsApp.Auth.TokenManager do
  use GenServer
  alias NewsApp.Auth.JwtClient

  # Client API
  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{token: nil, expires_at: nil}, name: __MODULE__)
  end

  def get_valid_token do
    GenServer.call(__MODULE__, :get_token)
  end

  # Server callbacks
  def init(state) do
    # Fetch initial token on startup
    {:ok, refresh_token(state)}
  end

  def handle_call(:get_token, _from, %{token: token, expires_at: exp} = state) do
    now = DateTime.utc_now() |> DateTime.to_unix()
    
    # Refresh token if it expires in less than 5 minutes
    state = if exp - now < 300, do: refresh_token(state), else: state
    
    {:reply, state.token, state}
  end

  defp refresh_token(_state) do
    # Get new token from Strapi
    {:ok, %{token: token, expires_in: expires_in}} = JwtClient.fetch_token()
    expires_at = DateTime.utc_now() |> DateTime.add(expires_in, :second) |> DateTime.to_unix()
    
    # Schedule token refresh before expiration
    Process.send_after(self(), :refresh_token, (expires_in - 300) * 1000)
    
    %{token: token, expires_at: expires_at}
  end
end

A dedicated GenServer polls Strapi every 10 minutes for fresh content. When new articles publish, Phoenix Channels notify subscribers immediately:

defmodule NewsApp.ContentSupervisor do
  use Supervisor

  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  def init(_init_arg) do
    children = [
      {NewsApp.ContentPoller, poll_interval: 10 * 60 * 1000},
      {NewsApp.CacheWarmer, schedule: [hour: [8, 12, 16]]}
    ]
    Supervisor.init(children, strategy: :one_for_one)
  end
end

defmodule NewsApp.ContentPoller do
  use GenServer
  alias NewsApp.StrapiClient
  alias NewsApp.Endpoint

  # Client API
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  # Server callbacks
  def init(opts) do
    schedule_poll(0) # Poll immediately on startup
    {:ok, %{poll_interval: opts[:poll_interval]}}
  end

  def handle_info(:poll, state) do
    # Poll for new content
    with {:ok, articles} <- StrapiClient.fetch_articles() do
      # Find new articles by comparing with previous fetch
      new_articles = find_new_articles(articles)
      
      # Broadcast to Phoenix channels if new articles exist
      if length(new_articles) > 0 do
        Endpoint.broadcast("content:updates", "new_articles", %{articles: new_articles})
      end
    end

    schedule_poll(state.poll_interval)
    {:noreply, state}
  end

  defp schedule_poll(interval) do
    Process.send_after(self(), :poll, interval)
  end
end

The Redis-based caching system reduces API load and speeds up content delivery:

defmodule NewsApp.Cache do
  alias NewsApp.Redis

  @default_ttl 60 * 60 * 2 # 2 hours in seconds

  def get_articles(filters \\ nil) do
    cache_key = build_key("articles", filters)
    
    case Redis.get(cache_key) do
      {:ok, nil} -> 
        # Cache miss - fetch from Strapi and cache result
        articles = NewsApp.StrapiClient.fetch_articles(filters)
        set_articles(articles, filters)
        articles
      {:ok, data} -> 
        # Cache hit
        Jason.decode!(data)
      {:error, reason} ->
        # Redis error - fallback to direct API call
        Logger.error("Redis cache error: #{inspect(reason)}")
        NewsApp.StrapiClient.fetch_articles(filters)
    end
  end

  def set_articles(articles, filters \\ nil) do
    cache_key = build_key("articles", filters)
    Redis.set(cache_key, Jason.encode!(articles), ex: @default_ttl)
  end

  def invalidate_articles(article_id \\ nil) do
    if article_id do
      # Invalidate specific article
      Redis.del(build_key("articles", %{id: article_id}))
    else
      # Invalidate all articles using pattern matching
      Redis.eval("return redis.call('DEL', unpack(redis.call('KEYS', ARGV[1])))", 
                0, "articles:*")
    end
  end

  defp build_key(resource, nil), do: "#{resource}:all"
  defp build_key(resource, filters) when is_map(filters) do
    filter_string = filters
                    |> Enum.map(fn {k, v} -> "#{k}:#{v}" end)
                    |> Enum.join(":")
    "#{resource}:#{filter_string}"
  end
end

The system implements circuit breakers to prevent cascade failures when Strapi is unavailable:

defmodule NewsApp.CircuitBreaker do
  use GenServer
  
  @timeout 5_000         # Circuit reset timeout
  @threshold 5           # Number of failures before opening circuit
  @retry_window 60_000   # Time window for retry after circuit opens

  # Client API
  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def call(service, func, args) do
    case GenServer.call(__MODULE__, {:check, service}) do
      :ok -> 
        try do
          result = apply(func, args)
          GenServer.cast(__MODULE__, {:success, service})
          {:ok, result}
        rescue
          e -> 
            GenServer.cast(__MODULE__, {:failure, service})
            {:error, e}
        end
      {:error, :circuit_open} ->
        {:error, :service_unavailable}
    end
  end

  # Server callbacks
  def init(_) do
    {:ok, %{circuits: %{}}}
  end

  def handle_call({:check, service}, _from, state) do
    circuit = Map.get(state.circuits, service, %{status: :closed, failures: 0, last_failure: nil})
    
    response = case circuit.status do
      :closed -> :ok
      :open ->
        now = System.monotonic_time(:millisecond)
        last_failure = circuit.last_failure || 0
        
        if now - last_failure > @retry_window do
          # Try half-open state
          :ok
        else
          {:error, :circuit_open}
        end
    end
    
    {:reply, response, state}
  end

  def handle_cast({:success, service}, state) do
    circuits = Map.update(state.circuits, service, %{status: :closed, failures: 0}, fn circuit ->
      %{circuit | status: :closed, failures: 0}
    end)
    
    {:noreply, %{state | circuits: circuits}}
  end

  def handle_cast({:failure, service}, state) do
    now = System.monotonic_time(:millisecond)
    
    circuits = Map.update(state.circuits, service, 
      %{status: :closed, failures: 1, last_failure: now}, 
      fn circuit ->
        failures = circuit.failures + 1
        status = if failures >= @threshold, do: :open, else: circuit.status
        
        %{circuit | 
          status: status, 
          failures: failures, 
          last_failure: now}
      end)
    
    {:noreply, %{state | circuits: circuits}}
  end
end

Strapi Open Office Hours

If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.

For more details, visit the Strapi documentation and the Elixir documentation.

Frequently Asked Questions

Yes, Elixir is exceptionally well-suited for real-time applications. Its lightweight process model and actor-based concurrency enable it to manage millions of concurrent connections efficiently, making it perfect for applications that require real-time updates, like messaging apps, online games, and live notifications.

Elixir's functional programming model offers several benefits for web development, including immutable data structures that reduce bugs and eliminate race conditions and hot code swapping for updating applications without downtime. These features contribute to building concurrent, scalable, and maintainable software, improving both the development process and application performance.

Best practices for securing your Elixir-Strapi integration include implementing proper authentication, such as JWT-based API communication with token rotation, configuring CORS correctly, and using HTTPS for secure data transmission. Additionally, managing API tokens securely and implementing rate limiting and input validation are crucial for protecting your application.

Yes, Strapi supports both REST and GraphQL APIs, allowing you to choose the best option for your Elixir application's needs. You can even use REST and GraphQL side by side to leverage the strengths of both API styles, optimizing performance and flexibility in data fetching and manipulation.

For smaller deployments, Docker Compose is a practical choice, allowing for easy setup and scalability. For larger applications, deploying with Kubernetes and Docker can provide independent scaling, resource isolation, and better handling of high availability and load balancing. Monitoring tools like Prometheus and Grafana are recommended for maintaining application health and performance.

Support and community discussions can be found in Strapi's official forums, the Elixir Forum, and through Strapi Open Office Hours. These platforms offer a space to connect with other developers, share experiences, and resolve integration challenges, providing valuable insights into best practices and troubleshooting for Elixir-Strapi projects.