DEV Community

Cover image for Speeding up Elixir: integration with native code (NIF, Ports, etc.)
Artyom Molchanov
Artyom Molchanov

Posted on

3 1 2 1 2

Speeding up Elixir: integration with native code (NIF, Ports, etc.)

⚠️ Important: Before introducing new languages into your project, ensure that your existing Elixir code has been optimized first. In many cases, proper optimization can significantly improve application performance and sometimes yield better results than adding another programming language.

Elixir and Erlang when are excellent languages for developing scalable and fault-tolerant systems. However, there may come times when you need to extract maximum performance or utilize libraries available only in other languages. If you're facing such situations or simply want to explore how to combine two favorite programming languages, this article is for you!


🔍 Search When You Need Native Code Integration

Before diving into technical details, it's important to understand when native code integration should be considered:

1. Compute-intensive Tasks

The BEAM virtual machine excels at concurrency but isn't designed for heavy computations. Consider integrating native code if your bottleneck involves CPU-bound operations:

  • 🧮 Mathematical Calculations—Linear algebra or matrix operations
  • 🔐 Cryptography—Encryption/decryption of large datasets
  • 🎨 Media Processing—Images, videos, audio
  • 🧠 Machine Learning—Model inference, vector operations

2. Hardware Interaction

When low-level access to hardware is required:

  • 📟 Embedded Systems—Raspberry Pi, microcontrollers (Note: While Nerves exists in Elixir, it might fall short)
  • 🎮 Specific Drivers—Unconventional devices
  • 📊 GPU Computing—CUDA, OpenCL

3. Reusing Existing Codebases

  • 🏛️ Proven Libraries on C/C++
  • 📚 Ecosystem Advantages from other languages (e.g., Python for ML)
  • 🔧 Avoid Reinventing the Wheel

4. Profiling and Bottlenecks

Typical signs indicating it's time to consider native code:

# Before Optimization - 1000ms
defmodule SlowModule do
  def process_data(data) do
    # Potential bottleneck candidate for native optimization
    Enum.reduce(data, 0, fn x, acc -> complex_calculation(x) + acc end)
  end

  defp complex_calculation(x) do
    # Imagine CPU-heavy calculations here
    # That don't scale well within BEAM
  end
end

# Exhausted all possible optimizations in Elixir and still slow?
# Perhaps it's time to introduce native code!
Enter fullscreen mode Exit fullscreen mode

Native extensions transform Elixir into an all-purpose tool where BEAM handles everything except critical computations. This opens doors to ML, hardware, video processing, and more speed-critical tasks.


Balance Comparison of Integration Methods

Choosing the right method of integration is crucial for project success. Here's a comparative table of methods with their pros and cons:

Method Speed BEAM Safety Implementation Complexity Language Support Communication Overhead Asynchronicity Ideal Use Cases использования
NIF ⚡⚡⚡⚡⚡ ❌ Dangerous 🔧🔧🔧 Medium C, C++ None (direct call) ❌ Blocks scheduler Microservices, lengthy CPU-bound tasks
Dirty NIF ⚡⚡⚡⚡ ⚠️ Conditionally Safe 🔧🔧🔧 Medium C, C++ None (direct call) ✅ Doesn't block primary scheduler Long computational tasks (>1ms)
Port ⚡⚡ ✅✅ Completely Safe 🔧 Simple Any High (IPC) ✅ Process isolation Python, Go, Bash-scripts
Port Driver ⚡⚡⚡⚡ ✅ Safe 🔧🔧🔧🔧 Complex C, C++ Low ✅ Dedicated thread Video/audio processing
gRPC ⚡⚡ ✅✅ Completely Safe 🔧🔧 Medium Any with gRPC support Moderate (network) ✅ Separate service Microservice architecture
Rustler ⚡⚡⚡⚡ ✅ Safe 🔧🔧 Medium Rust None (direct call) ✅ Supports asynchronous APIs Alternative to C NIF
Zigler ⚡⚡⚡⚡⚡ ✅ Safe 🔧🔧 Medium Zig None (direct call) ✅ Safer than standard NIFs Alternative to C in NIF

Visual Comparison by Key Metrics

Speed:               Safety:               Simplicity:
NIF         █████    Port        █████     Port        █████
Dirty NIF   ████     Dirty NIF   ███       gRPC        ████
Rustler     ████     Rustler     ████      Rustler     ████
Zigler      █████    Zigler      ████      Zigler      ████
Port Driver ████     Port Driver ████      NIF         ███
Port        ██       gRPC        █████     Port Driver █
gRPC        ██       NIF         █         Dirty NIF   ███
Enter fullscreen mode Exit fullscreen mode

Mechanisms Behind Integration


🧠 NIFs — Maximal Performance

Native Implemented Functions (NIFs) offer the fastest way to integrate native code, albeit being the riskiest option. They execute directly in the BEAM scheduler's thread, ensuring lightning-fast execution due to zero overhead.

How Do NIFs Work?

  • Compilation: Native code (C/Rust/Zig) compiles into a dynamic library (.so, .dll)
  • Loading: BEAM loads the library during module startup via :erlang.load_nif/2
  • Direct Execution: Functions run in the same thread as the calling Elixir code

Benefits

  • Blazing Speed: Calls take approximately 0.1–1 µs (~100–1000× faster than Ports)
  • Access to BEAM API: Direct interaction with Erlang terms
  • No Serialization: No encoding/decoding costs
  • Distribution: Library ships alongside the OTP app

Drawbacks

  • Risk of VM Crash: Errors in NIF terminate the entire BEAM instance
  • Scheduler Blocking: Long-running NIFs freeze multithreading
  • Debugging Difficulty: Memory leaks hard to detect
  • Platform Dependency: Requires compilation per target architecture

Dirty NIFs — Safer Alternative

With Erlang/OTP 20+, we have Dirty NIFs, which execute in dedicated thread pools, allowing longer computations without blocking the BEAM scheduler.

// Defining a Dirty NIF
static ErlNifFunc nif_funcs[] = {
    {"long_computation", 1, long_computation_nif, ERL_NIF_DIRTY_CPU}
};
Enter fullscreen mode Exit fullscreen mode

Tip: Use ERL_NIF_DIRTY_CPU for CPU-bound ops and ERL_NIF_DIRTY_IO for I/O-bound ones

🔌 Ports — Complete Isolation

Ports provide a safe mechanism for interacting with external programs through standard I/O streams (stdin/stdout). It's the safest form of integration since the external program runs in its own OS process.

How Do Ports Work?

  • Launch: Elixir spawns an external program as a separate OS process
  • Communication: Data exchange occurs over stdin/stdout
  • Isolation: Failure of the external program doesn't affect BEAM

Benefits

  • Full Safety: Isolation ensures BEAM stability
  • Language Agnosticism: Works with any programming language
  • Ease of Debugging: External program can be tested independently
  • Dependency-Free: No specific BEAM-compatible libraries needed

Downsides

  • High Overheads: Approximately 100–500 µs per invocation
  • Serialization Required: Needs conversion of data (usually to JSON)
  • Blocking Calls: By default blocks the caller process

🚗 Port Drivers — Goldilocks Zone

Port Drivers are high-performance alternatives to Ports, though they require greater complexity. These drivers are written in C and embedded directly into BEAM’s address space, running in separate threads.

How Do Port Drivers Work?

  • Load: BEAM loads the C library into its memory space
  • Thread Allocation: Each driver operates in its own thread
  • Asynchronous Operation: Data exchanges occur via message queues

Benefits

  • Performance: Significantly faster than regular ports
  • Safety: Lower risk compared to NIFs
  • Async Capability: Non-blocking operation supported
  • Convenience: No need to spawn additional processes

Disadvantages

  • Complexity: Requires knowledge of Erlang C API
  • Limited Scope: Only works with C/C++
  • Less Documentation: Few examples and guides available

🐊 Zigler — Wrapper Around NIFs for Zig

Zigler is a library enabling writing native extensions using the Zig programming language. It integrates the Zig compiler directly into the Elixir compilation cycle, allowing direct coding in Zig inside Elixir modules.

Benefits

  • Minimal overhead for native calls
  • Memory safety without garbage collection
  • Direct access to low-level operations
  • Simpler compilation compared to other native extensions
  • Hot reloading capability

Disadvantages

  • Zig is relatively young with a smaller community
  • Higher entry barrier for Elixir developers
  • Limited ecosystem of libraries compared to Rust

🦀 Rustler — Wrapping NIFs for Rust

Rustler is a library for creating native extensions in Erlang/Elixir using Rust. It provides a safe binding between Rust and Erlang/Elixir, facilitating writing NIFs in Rust.

Benefits

  • Memory safety enforced at compile-time
  • High performance for computationally intensive tasks
  • Rich ecosystem of Rust packages (crates)
  • Protection against crashes in native code (protects BEAM from crashing)
  • Parallel execution without blocking the BEAM scheduler

Disadvantages

  • Complicated build process and dependency management
  • Requires familiarity with two different programming paradigms
  • Possible compatibility issues across versions
  • Potential bottlenecks at runtime boundaries

🌐 Global gRPC and Similar Protocols

For more advanced scenarios involving external services, especially in microservice architectures, protocols like gRPC provide structured and scalable ways of integration.

gRPC Benefits for Elixir

  • Contract Schema: Strong typing via Protocol Buffers
  • Bidirectional Streaming: Supports streaming both ways
  • Cross-platform: Supports multiple languages
  • Performance: More efficient than REST/JSON

❗️ An example using gRPC was omitted due to its extensive nature. For detailed info about gRPC in Elixir, see elixir-grpc.


🧪 Practical Examples of Integration

📚 The goal of this article is to give an overview of available native integration methods. Deep implementations and edge cases are omitted to maintain balance between theory and practical application.

Exploring Criteria for Choosing Integration Methods

Before moving onto examples, let's define key factors influencing our choice:

  1. Performance: How fast does the call execute?
  2. Safety: Risk level for BEAM stability
  3. Implementation Complexity: Ease of implementation
  4. Language Support: Suitability of the chosen language with BEAM

⚙️ C — Maximum Performance Using NIF and Port Drivers

Optimal Methods:

  • NIF — for instantaneous operations (<1ms)
  • Port Drivers — for long-lasting or async tasks

Why Choose This Approach?
C offers seamless compatibility with BEAM, making it ideal for:

  • Critically performant tasks
  • Low-level operations (hardware, GPU)
  • Integrating pre-existing C libraries

Example 1: Fast NIF for Hashing (Best Suited for C)

// hash_nif.c
#include <erl_nif.h>
#include <openssl/sha.h>

static ERL_NIF_TERM sha256_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifBinary input;
    if (!enif_inspect_binary(env, argv[0], &input)) {
        return enif_make_badarg(env);
    }

    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256(input.data, input.size, hash);

    ErlNifBinary output;
    enif_alloc_binary(SHA256_DIGEST_LENGTH, &output);
    memcpy(output.data, hash, SHA256_DIGEST_LENGTH);

    return enif_make_binary(env, &output);
}

static ErlNifFunc nif_funcs[] = {
    {"sha256", 1, sha256_nif}
};

ERL_NIF_INIT(Elixir.CryptoNif, nif_funcs, NULL, NULL, NULL, NULL)
Enter fullscreen mode Exit fullscreen mode

Elixir Part:

defmodule CryptoNif do
  @on_load :load_nif
  def load_nif do
    :erlang.load_nif(Path.expand("./hash_nif"), 0)
  end

  def sha256(_data), do: raise "NIF not loaded!"
end
Enter fullscreen mode Exit fullscreen mode

Compiling C Source:
You'll need the OpenSSL library installed:

# For Ubuntu/Debian
sudo apt-get install libssl-dev

# For CentOS/RHEL
sudo yum install openssl-devel

# For macOS (with Homebrew)
brew install openssl
Compile the C source:


gcc -shared -fPIC -o hash_nif.so hash_nif.c \
  -I /usr/lib/erlang/erts-15.2.7/include \ # Path depends on your system
  -I /usr/include/openssl \
  -L /usr/lib/openssl \
  -lssl -lcrypto
Enter fullscreen mode Exit fullscreen mode

Result:

iex(1)> CryptoNif.sha256("test")
<<159, 134, 208, 129, 136, 76, 125, 101, 154, 47, 234, 160, 197, 90, 208, 21, 163, 191, 79, 27, 43, 11, 130, 44, 209, 93, 108, 21, 176, 240, 10, 8>>
Enter fullscreen mode Exit fullscreen mode

Example 2: Port Driver for String Reversal

// reverse_driver.c
#include "erl_driver.h"
#include <string.h>

typedef struct {
    ErlDrvPort port;
} DriverData;

static void reverse_and_send(ErlDrvData drv_data, char* buf, ErlDrvSizeT len) {
    DriverData* d = (DriverData*)drv_data;

    for (ErlDrvSizeT i = 0; i < len / 2; i++) {
        char temp = buf[i];
        buf[i] = buf[len - 1 - i];
        buf[len - 1 - i] = temp;
    }

    driver_output(d->port, buf, len);
}

static ErlDrvData driver_start(ErlDrvPort port, char* command) {
    DriverData* d = (DriverData*)driver_alloc(sizeof(DriverData));
    d->port = port;
    return (ErlDrvData)d;
}

static void driver_stop(ErlDrvData drv_data) {
    DriverData* d = (DriverData*)drv_data;
    driver_free(d);
}

static ErlDrvEntry driver_entry = {
    .init = NULL,
    .start = driver_start,
    .stop = driver_stop,
    .output = reverse_and_send,
    .ready_input = NULL,
    .ready_output = NULL,
    .driver_name = "reverse_driver",
    .finish = NULL,
    .handle = NULL,
    .control = NULL,
    .timeout = NULL,
    .outputv = NULL,
    .ready_async = NULL,
    .flush = NULL,
    .call = NULL,
    .extended_marker = ERL_DRV_EXTENDED_MARKER,
    .major_version = ERL_DRV_EXTENDED_MAJOR_VERSION,
    .minor_version = ERL_DRV_EXTENDED_MINOR_VERSION,
    .driver_flags = 0,
    .handle2 = NULL,
    .process_exit = NULL,
    .stop_select = NULL
};


DRIVER_INIT(reverse_driver) {
    return &driver_entry;
}
Enter fullscreen mode Exit fullscreen mode

Elixir Part:

defmodule ReverseString do
  def start() do
    :ok = :erl_ddll.load_driver('./', 'reverse_driver')
    Port.open({:spawn, 'reverse_driver'}, [:binary])
  end

  def reverse(port, string) when is_binary(string) do
    true = Port.command(port, string)
    receive do
      {^port, {:data, result}} -> result
    after
      1000 -> {:error, :timeout}
    end
  end

  def stop(port) do
    Port.close(port)
  end
end
Enter fullscreen mode Exit fullscreen mode

Compiling C Source:

gcc -std=gnu99 -shared -fPIC -o reverse_driver.so reverse_driver.c \
    -I/usr/lib/erlang/erts-15.2.7/include/ # Path depends on your system
Enter fullscreen mode Exit fullscreen mode

Result:

iex(1)> port = ReverseString.start
#Port<0.6>
iex(2)> ReverseString.reverse(port, "hello")
"olleh"
Enter fullscreen mode Exit fullscreen mode

🦀 Rustler — Security and Performance with Rustler

Optimal Method: Rustler (specialized wrapper for NIF)

Why Rustler?

  • Full memory safety
  • Convenient macros for working with BEAM terms
  • Automatic error handling
  • Async task support

Example: Parallel Data Processing

Add Rustler to mix.exs (Current version is 0.36.1):

  defp deps do
    [
      {:rustler, "~> 0.36.1"}
    ]
  end
Enter fullscreen mode Exit fullscreen mode

Create Rust Project Inside Elixir:

mix deps.get
mix rustler.new

This is the name of the Elixir module the NIF module will be registered to.
Module name > RustUtils
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (rustutils) > (Enter)
Enter fullscreen mode Exit fullscreen mode
// lib.rs
use rayon::prelude::*;

#[rustler::nif]
fn parallel_double(input: Vec<i64>) -> Vec<i64> {
    input.par_iter().map(|&x| x * 2).collect()
}

rustler::init!("Elixir.RustUtils");
Enter fullscreen mode Exit fullscreen mode

Elixir Part:

defmodule RustUtils do
  use Rustler, otp_app: :rust_utils, crate: "rustutils"

  def parallel_double(_list), do: :erlang.nif_error(:not_loaded)
end
Enter fullscreen mode Exit fullscreen mode

Result:

RustUtils.parallel_double([1, 2, 3])
# => [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

3. 🐍 Python — Effortless Integration Through Ports

Optimal Method: Ports

Why Ports?

  • Total process isolation
  • Easy debugging
  • Access to Python ecosystem
  • Handles long operations (like ML models)

Example: Creating Your Own Model and Integrating TensorFlow

Build the Model:

# create_model.py
import tensorflow as tf
import numpy as np

model = tf.keras.Sequential([
    tf.keras.layers.Dense(1, input_shape=(3,), use_bias=False)
])

model.set_weights([np.array([[1.0], [1.0], [1.0]])])

model.save("my_model.h5")
print("✅ Model saved to my_model.h5")
Enter fullscreen mode Exit fullscreen mode
#tensorflow_port.py
import sys
import json
import numpy as np
import tensorflow as tf
import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
tf.get_logger().setLevel('ERROR')

model = tf.keras.models.load_model('my_model.h5')

def predict(data):
    try:
        if isinstance(data, str):
            data = json.loads(data)

        input_array = np.array(data, dtype=np.float32).reshape(1, 3)
        prediction = model.predict(input_array)
        return json.dumps({"status": "success", "result": prediction.tolist()})
    except Exception as e:
        return json.dumps({"status": "error", "message": str(e)})

if __name__ == "__main__":
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue

        try:
            response = predict(line)
            sys.stdout.write(response + "\n")
            sys.stdout.flush()
        except Exception as e:
            error = json.dumps({"status": "error", "message": str(e)})
            sys.stdout.write(error + "\n")
            sys.stdout.flush()
Enter fullscreen mode Exit fullscreen mode

Elixir Part:

defmodule TensorflowPort do
  @timeout 5_000

  def start do
    Port.open(
      {:spawn, "python3 tensorflow_port.py"},
      [:binary, :use_stdio, :exit_status, :stderr_to_stdout, {:line, 1024}]
    )
  end

  def predict(port, input_data) do
    input_data
    |> Jason.encode!()
    |> then(&Port.command(port, &1 <> "\n"))

    wait_for_response(port)
  end

  defp wait_for_response(port) do
    receive do
      {^port, {:data, {:eol, line}}} ->
        case Jason.decode(line) do
          {:ok, %{"status" => "success", "result" => result}} -> {:ok, result}
          {:ok, %{"status" => "error", "message" => msg}} -> {:error, msg}
          _ -> wait_for_response(port)
        end

      {^port, {:exit_status, status}} ->
        {:error, "Python process exited with status #{status}"}
    after
      @timeout -> {:error, :timeout}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Result:

To run the script, enter the Python environment and launch iex -S mix.

(venv)
iex(1)> port = TensorflowPort.start
#Port<0.10>
iex(2)> TensorflowPort.predict(port, [1,2,3])
{:ok, [[6.0]]}
Enter fullscreen mode Exit fullscreen mode

4. 🦫 Go — Efficiency Through CGO or gRPC

Optimal Methods:

  • CGO wrappers for NIF — best for max performance
  • gRPC — suitable for complex interactions and microservices

CGO Wrapper for Go Code (Suitable for Performance)

// fib.go
package main

import "C"

func GoFib(n C.int) C.int {
    a, b := 0, 1
    for i := 0; i < int(n); i++ {
        a, b = b, a+b
    }
    return C.int(a)
}

func main() {}
Enter fullscreen mode Exit fullscreen mode

C Wrapper:

// c_src/go_nif.c
#include <erl_nif.h>
#include "libfib.h"

static ERL_NIF_TERM go_fib_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    int n;
    if (!enif_get_int(env, argv[0], &n)) {
        return enif_make_badarg(env);
    }
    return enif_make_int(env, GoFib(n));
}

static ErlNifFunc nif_funcs[] = {
    {"go_fib", 1, go_fib_nif}
};

ERL_NIF_INIT(Elixir.NifGo, nif_funcs, NULL, NULL, NULL, NULL)
Enter fullscreen mode Exit fullscreen mode

Elixir part:

defmodule NifGo do
  @on_load :load_nif

  def load_nif do
    nif_path = Path.expand("priv/go_nif", File.cwd!())

    # Preload Go library
    :erlang.load_nif(Path.expand("priv/libfib", File.cwd!()), 0)

    # Load main NIF library
    :erlang.load_nif(nif_path, 0)
  end

  def go_fib(_n), do: raise("NIF not loaded!")
end
Enter fullscreen mode Exit fullscreen mode

Compile:

go build -buildmode=c-shared -o priv/libfib.so fib.go

gcc -shared -fPIC \
    -I/usr/lib/erlang/erts-15.2.7/include/ \ # Path varies based on your system
    -I./priv \
    -o priv/go_nif.so \
    c_src/go_nif.c \
    ./priv/libfib.so \
    -Wl,-rpath,'$ORIGIN'

LD_LIBRARY_PATH=./priv iex -S mix
Enter fullscreen mode Exit fullscreen mode

Run:

iex(1)> NifGo.go_fib(3)
2
Enter fullscreen mode Exit fullscreen mode

⚠️ If you'd rather avoid dealing with CGO, opt for gRPC or Ports instead. They're safer, although slower.

5. 🐊 Zig — Integration Via Zigler

Optimal Method: Zigler (wrapper for Zig-based NIFs)

Why Zigler?

  • Minimum overhead for native calls
  • Memory safety without garbage collection
  • Direct access to low-level operations
  • Simplified compilation relative to other native extensions
  • Hot-reloading capabilities

Example: Quick Binary Data Handling

Add Zigler to mix.exs:

def deps do
  [
    {:zigler, "~> 0.13.2", runtime: false}
  ]
end
Enter fullscreen mode Exit fullscreen mode

lib/nif_zig.ex:

defmodule NifZig do
  use Zig, otp_app: :zigler

  ~Z"""
  pub fn string_count(string: []u8) i64 {
    return @intCast(string.len);
  }

  pub fn list_sum(array: []f64) f64 {
    var sum: f64 = 0.0;
    for(array) | item | {
      sum += item;
    }
    return sum;
  }
  """
end
Enter fullscreen mode Exit fullscreen mode

Result:

iex(3)> NifZig.string_count("hello")
5
iex(4)> NifZig.list_sum([1.2,2.3,3.4,4.5])
11.4
Enter fullscreen mode Exit fullscreen mode
Language Optimal Method Alternatives When To Use
C NIF, Port Drivers Ports Maximum performance
Rust Rustler (NIF) - Safety + performance
Python Ports gRPC Integration with ML/scientific tools
Go C-обёртки (NIF), gRPC Ports Leveraging Go ecosystem
Zig Zigler (NIF) - Modern alternative to C

Final Thoughts

Integrating Elixir with native code unlocks new possibilities for developers, combining BEAM's strengths (scalability, reliability) with the performance and libraries of other languages. We've explored various integration techniques: NIF, Dirty NIF, Ports, Port Drivers, Rustler, Zigler, and gRPC, along with their optimal usage scenarios.

Remember, choosing an integration method should align with your project's requirements regarding performance, security, and ease of development.

Author's Notes

Thank you for reading this article! I hope you've found it insightful learning about ways to integrate other languages into Elixir. Among these options, I found Port Drivers particularly challenging, as crafting one turned out to be quite an adventure, requiring hours of trial-and-error to find the correct approach. Overall, however, integrating other languages into Elixir proved neither difficult nor boring—it was actually fun!

If you spot inaccuracies or have interesting additions, please share them in the comments section. Constructive feedback is always appreciated 😎

DevCycle image

Ship Faster, Stay Flexible.

DevCycle is the first feature flag platform with OpenFeature built-in to every open source SDK, designed to help developers ship faster while avoiding vendor-lock in.

Start shipping

Top comments (0)

AWS Q Developer image

Build your favorite retro game with Amazon Q Developer CLI in the Challenge & win a T-shirt!

Feeling nostalgic? Build Games Challenge is your chance to recreate your favorite retro arcade style game using Amazon Q Developer’s agentic coding experience in the command line interface, Q Developer CLI.

Participate Now

👋 Kindness is contagious

Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. Every developer’s experience matters—add your thoughts and help us grow together.

A simple “thank you” can uplift the author and spark new discussions—leave yours below!

On DEV, knowledge-sharing connects us and drives innovation. Found this useful? A quick note of appreciation makes a real impact.

Okay