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.
A client is created using Tesla.client/2
, which takes a list of middleware
and an adapter.
client = Tesla.client([Tesla.Middleware.PathParams, Tesla.Middleware.Logger])
You can then use the client to make requests:
Tesla.get(client, "/users/123")
You can pass options to middleware by registering the middleware as a tuple of two elements, where the first element is the middleware module and the second is the options.
client = Tesla.client(
[{Tesla.Middleware.BaseUrl, "https://api.example.com"}]
)
By default, the global adapter is used. You can override this by passing an adapter to the client.
client = Tesla.client([], Tesla.Adapter.Mint)
You can also pass options to the adapter.
client = Tesla.client([], {Tesla.Adapter.Mint, pool: :my_pool})
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"})
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"})
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.