These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
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:
1defp deps do
2 [
3 {:httpoison, "~> 2.0"},
4 {:jason, "~> 1.4"}
5 ]
6end
Configure environment variables for API keys and database credentials from the start. Never hardcode sensitive values.
Keep in touch with the latest Strapi and Elixir updates
Creating a Strapi Project
Create a new Strapi project with the CLI:
1npx create-strapi-app@latest my-cms-project --quickstart
For production-like development, use Docker Compose:
1version: '3'
2services:
3 strapi:
4 image: strapi/strapi
5 environment:
6 DATABASE_CLIENT: postgres
7 DATABASE_HOST: db
8 DATABASE_PORT: 5432
9 DATABASE_NAME: strapi
10 DATABASE_USERNAME: strapi
11 DATABASE_PASSWORD: strapi
12 volumes:
13 - ./app:/srv/app
14 ports:
15 - '1337:1337'
16 depends_on:
17 - db
18
19 db:
20 image: postgres:13
21 environment:
22 POSTGRES_DB: strapi
23 POSTGRES_USER: strapi
24 POSTGRES_PASSWORD: strapi
25 volumes:
26 - strapi-data:/var/lib/postgresql/data
27
28volumes:
29 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:
1export STRAPI_API_TOKEN="your_generated_token_here"
2export 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:
1defmodule MyApp.StrapiClient do
2 @base_url System.get_env("STRAPI_BASE_URL")
3 @api_token System.get_env("STRAPI_API_TOKEN")
4
5 def get_articles do
6 "/api/articles"
7 |> build_url()
8 |> HTTPoison.get!(headers())
9 |> handle_response()
10 end
11
12 def get_article(id) do
13 "/api/articles/#{id}"
14 |> build_url()
15 |> HTTPoison.get!(headers())
16 |> handle_response()
17 end
18
19 defp build_url(path), do: @base_url <> path
20
21 defp headers do
22 [
23 {"Authorization", "Bearer #{@api_token}"},
24 {"Content-Type", "application/json"}
25 ]
26 end
27
28 defp handle_response(%HTTPoison.Response{status_code: 200, body: body}) do
29 Jason.decode!(body)
30 end
31
32 defp handle_response(%HTTPoison.Response{status_code: status_code}) do
33 {:error, "API request failed with status #{status_code}"}
34 end
35end
Keep in touch with the latest Strapi and Elixir updates
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:
1defmodule NewsApp.StrapiClient do
2 @strapi_url System.get_env("STRAPI_URL")
3 @jwt System.get_env("STRAPI_JWT")
4
5 def fetch_articles do
6 HTTPoison.get!(
7 "#{@strapi_url}/api/articles?populate=*",
8 [{"Authorization", "Bearer #{@jwt}"}],
9 recv_timeout: 5_000
10 )
11 |> handle_response()
12 |> cache_articles()
13 end
14end
JWT authentication implementation with automatic token rotation ensures secure API communication:
1defmodule NewsApp.Auth.TokenManager do
2 use GenServer
3 alias NewsApp.Auth.JwtClient
4
5 # Client API
6 def start_link(_opts) do
7 GenServer.start_link(__MODULE__, %{token: nil, expires_at: nil}, name: __MODULE__)
8 end
9
10 def get_valid_token do
11 GenServer.call(__MODULE__, :get_token)
12 end
13
14 # Server callbacks
15 def init(state) do
16 # Fetch initial token on startup
17 {:ok, refresh_token(state)}
18 end
19
20 def handle_call(:get_token, _from, %{token: token, expires_at: exp} = state) do
21 now = DateTime.utc_now() |> DateTime.to_unix()
22
23 # Refresh token if it expires in less than 5 minutes
24 state = if exp - now < 300, do: refresh_token(state), else: state
25
26 {:reply, state.token, state}
27 end
28
29 defp refresh_token(_state) do
30 # Get new token from Strapi
31 {:ok, %{token: token, expires_in: expires_in}} = JwtClient.fetch_token()
32 expires_at = DateTime.utc_now() |> DateTime.add(expires_in, :second) |> DateTime.to_unix()
33
34 # Schedule token refresh before expiration
35 Process.send_after(self(), :refresh_token, (expires_in - 300) * 1000)
36
37 %{token: token, expires_at: expires_at}
38 end
39end
A dedicated GenServer
polls Strapi every 10 minutes for fresh content. When new articles publish, Phoenix Channels notify subscribers immediately:
1defmodule NewsApp.ContentSupervisor do
2 use Supervisor
3
4 def start_link(init_arg) do
5 Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
6 end
7
8 def init(_init_arg) do
9 children = [
10 {NewsApp.ContentPoller, poll_interval: 10 * 60 * 1000},
11 {NewsApp.CacheWarmer, schedule: [hour: [8, 12, 16]]}
12 ]
13 Supervisor.init(children, strategy: :one_for_one)
14 end
15end
16
17defmodule NewsApp.ContentPoller do
18 use GenServer
19 alias NewsApp.StrapiClient
20 alias NewsApp.Endpoint
21
22 # Client API
23 def start_link(opts) do
24 GenServer.start_link(__MODULE__, opts, name: __MODULE__)
25 end
26
27 # Server callbacks
28 def init(opts) do
29 schedule_poll(0) # Poll immediately on startup
30 {:ok, %{poll_interval: opts[:poll_interval]}}
31 end
32
33 def handle_info(:poll, state) do
34 # Poll for new content
35 with {:ok, articles} <- StrapiClient.fetch_articles() do
36 # Find new articles by comparing with previous fetch
37 new_articles = find_new_articles(articles)
38
39 # Broadcast to Phoenix channels if new articles exist
40 if length(new_articles) > 0 do
41 Endpoint.broadcast("content:updates", "new_articles", %{articles: new_articles})
42 end
43 end
44
45 schedule_poll(state.poll_interval)
46 {:noreply, state}
47 end
48
49 defp schedule_poll(interval) do
50 Process.send_after(self(), :poll, interval)
51 end
52end
The Redis-based caching system reduces API load and speeds up content delivery:
1defmodule NewsApp.Cache do
2 alias NewsApp.Redis
3
4 @default_ttl 60 * 60 * 2 # 2 hours in seconds
5
6 def get_articles(filters \\ nil) do
7 cache_key = build_key("articles", filters)
8
9 case Redis.get(cache_key) do
10 {:ok, nil} ->
11 # Cache miss - fetch from Strapi and cache result
12 articles = NewsApp.StrapiClient.fetch_articles(filters)
13 set_articles(articles, filters)
14 articles
15 {:ok, data} ->
16 # Cache hit
17 Jason.decode!(data)
18 {:error, reason} ->
19 # Redis error - fallback to direct API call
20 Logger.error("Redis cache error: #{inspect(reason)}")
21 NewsApp.StrapiClient.fetch_articles(filters)
22 end
23 end
24
25 def set_articles(articles, filters \\ nil) do
26 cache_key = build_key("articles", filters)
27 Redis.set(cache_key, Jason.encode!(articles), ex: @default_ttl)
28 end
29
30 def invalidate_articles(article_id \\ nil) do
31 if article_id do
32 # Invalidate specific article
33 Redis.del(build_key("articles", %{id: article_id}))
34 else
35 # Invalidate all articles using pattern matching
36 Redis.eval("return redis.call('DEL', unpack(redis.call('KEYS', ARGV[1])))",
37 0, "articles:*")
38 end
39 end
40
41 defp build_key(resource, nil), do: "#{resource}:all"
42 defp build_key(resource, filters) when is_map(filters) do
43 filter_string = filters
44 |> Enum.map(fn {k, v} -> "#{k}:#{v}" end)
45 |> Enum.join(":")
46 "#{resource}:#{filter_string}"
47 end
48end
The system implements circuit breakers to prevent cascade failures when Strapi is unavailable:
1defmodule NewsApp.CircuitBreaker do
2 use GenServer
3
4 @timeout 5_000 # Circuit reset timeout
5 @threshold 5 # Number of failures before opening circuit
6 @retry_window 60_000 # Time window for retry after circuit opens
7
8 # Client API
9 def start_link(_opts) do
10 GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
11 end
12
13 def call(service, func, args) do
14 case GenServer.call(__MODULE__, {:check, service}) do
15 :ok ->
16 try do
17 result = apply(func, args)
18 GenServer.cast(__MODULE__, {:success, service})
19 {:ok, result}
20 rescue
21 e ->
22 GenServer.cast(__MODULE__, {:failure, service})
23 {:error, e}
24 end
25 {:error, :circuit_open} ->
26 {:error, :service_unavailable}
27 end
28 end
29
30 # Server callbacks
31 def init(_) do
32 {:ok, %{circuits: %{}}}
33 end
34
35 def handle_call({:check, service}, _from, state) do
36 circuit = Map.get(state.circuits, service, %{status: :closed, failures: 0, last_failure: nil})
37
38 response = case circuit.status do
39 :closed -> :ok
40 :open ->
41 now = System.monotonic_time(:millisecond)
42 last_failure = circuit.last_failure || 0
43
44 if now - last_failure > @retry_window do
45 # Try half-open state
46 :ok
47 else
48 {:error, :circuit_open}
49 end
50 end
51
52 {:reply, response, state}
53 end
54
55 def handle_cast({:success, service}, state) do
56 circuits = Map.update(state.circuits, service, %{status: :closed, failures: 0}, fn circuit ->
57 %{circuit | status: :closed, failures: 0}
58 end)
59
60 {:noreply, %{state | circuits: circuits}}
61 end
62
63 def handle_cast({:failure, service}, state) do
64 now = System.monotonic_time(:millisecond)
65
66 circuits = Map.update(state.circuits, service,
67 %{status: :closed, failures: 1, last_failure: now},
68 fn circuit ->
69 failures = circuit.failures + 1
70 status = if failures >= @threshold, do: :open, else: circuit.status
71
72 %{circuit |
73 status: status,
74 failures: failures,
75 last_failure: now}
76 end)
77
78 {:noreply, %{state | circuits: circuits}}
79 end
80end
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.