View Source Client

In Tesla, a client is an entity that combines middleware and an adapter, created using Tesla.client/2. Middleware components modify or enhance requests and responses—such as adding headers or handling authentication—while adapters handle the underlying HTTP communication. For more details, see the sections on middleware and adapters.

Single-Client (Singleton) Pattern

A common approach in applications is to encapsulate client creation within a module or function that sets up standard middleware and adapter configurations. This results in a single, shared client instance used throughout the application. For example:

defmodule MyApp.ServiceName do
  defp client do
    middleware = [
      {Tesla.Middleware.BaseUrl, "https://api.service.com"},
      {Tesla.Middleware.BearerAuth, token: bearer_token()},
      # Additional middleware...
    ]
    Tesla.client(middleware, adapter())
  end

  defp adapter do
    Keyword.get(config(), :adapter)
  end

  defp bearer_token do
    Keyword.fetch!(config(), :bearer_token)
  end

  defp config do
    Application.get_env(:my_app, __MODULE__, [])
  end
end

In this pattern, the client is constructed internally, and operations use this singleton client:

defmodule MyApp.ServiceName do
  def operation_name(body) do
    url = "/endpoint"
    # The client() function is called internally
    response = Tesla.post!(client(), url, body)
    # Process the response...
  end

  defp client do
    # Client construction as shown earlier
  end
end

You can then use the module to make requests without managing the client externally:

{:ok, response} = MyApp.ServiceName.operation_name(%{key: "value"})

Multi-Client Pattern

In scenarios where different configurations are needed—such as multi-tenancy applications or interacting with multiple services—you can modify the client function to accept configuration parameters. This allows for the creation of multiple clients with varying settings:

defmodule MyApp.ServiceName do
  def operation_name(client, body) do
    url = "/endpoint"
    # The client is passed as a parameter
    response = Tesla.post!(client, url, body)
    # Process the response...
  end

  def client(opts) do
    middleware = [
      {Tesla.Middleware.BaseUrl, opts[:base_url]},
      {Tesla.Middleware.BearerAuth, token: opts[:bearer_token]},
      # Additional middleware...
    ]
    Tesla.client(middleware, opts[:adapter])
  end
end

Now, you can create clients with different configurations:

client = MyApp.ServiceName.client(
  base_url: "https://api.service.com",
  bearer_token: "token_value",
  adapter: Tesla.Adapter.Hackney
  # Additional options...
)
{:ok, response} = MyApp.ServiceName.operation_name(client, %{key: "value"})

Comparing Single-Client and Multi-Client Patterns

The choice between using a single-client (singleton) or multi-client pattern depends on your specific needs:

  • Library Authors: It's generally advisable to avoid the singleton client pattern. Hardcoding configurations can limit flexibility and hinder users in multi-tenant environments. Providing the ability to create clients with custom configurations makes your library more adaptable and user-friendly.

  • Application Developers: For simpler applications, a singleton client might suffice initially. However, adopting the multi-client approach from the outset can prevent future refactoring if your application grows or needs change.

Understanding these patterns helps you design applications and libraries that are flexible and maintainable, aligning with best practices in software development.