Shared Client Pattern

7 minute read

Why Share Clients?

When your provider has multiple resources and data sources, they all need to communicate with the same API. Instead of creating a new client for each resource, share a single configured client across all of them.

Benefits:

  • ✅ Single configuration point
  • ✅ Reuse HTTP connections (connection pooling)
  • ✅ Consistent authentication
  • ✅ Better performance
  • ✅ Easier testing

The Pattern

1. Provider Struct

type MyCloudProvider struct {
    version string
}

2. Configure Method Creates Client

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    // Parse configuration
    var config MyCloudProviderModel
    req.Config.Get(ctx, &config)
    
    // Create API client
    client := api.NewClient(api.Config{
        APIKey:   config.APIKey.ValueString(),
        Endpoint: config.Endpoint.ValueString(),
    })
    
    // Share with resources and data sources
    resp.ResourceData = client
    resp.DataSourceData = client
}

3. Resources Access Shared Client

func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    // Get shared client
    client := req.ProviderData.(*api.Client)
    
    // Use client to create resource
    env, err := client.CreateEnvironment(ctx, ...)
}

Complete Implementation

Provider Definition

package provider

import (
    "context"
    "os"
    
    "github.com/hashicorp/terraform-plugin-framework/datasource"
    "github.com/hashicorp/terraform-plugin-framework/provider"
    "github.com/hashicorp/terraform-plugin-framework/provider/schema"
    "github.com/hashicorp/terraform-plugin-framework/resource"
    "github.com/hashicorp/terraform-plugin-framework/types"
    
    "github.com/mycompany/mycloud-api-client/api"
)

// Provider struct
type MyCloudProvider struct {
    version string
}

// Provider configuration model
type MyCloudProviderModel struct {
    APIKey   types.String `tfsdk:"api_key"`
    Endpoint types.String `tfsdk:"endpoint"`
    Region   types.String `tfsdk:"region"`
    Timeout  types.Number `tfsdk:"timeout"`
}

// Ensure provider implements interfaces
var _ provider.Provider = &MyCloudProvider{}

func New(version string) func() provider.Provider {
    return func() provider.Provider {
        return &MyCloudProvider{
            version: version,
        }
    }
}

func (p *MyCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
    resp.TypeName = "mycloud"
    resp.Version = p.version
}

Provider Schema

func (p *MyCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        MarkdownDescription: "MyCloud provider for managing cloud infrastructure",
        
        Attributes: map[string]schema.Attribute{
            "api_key": schema.StringAttribute{
                Optional:            true,
                Sensitive:           true,
                MarkdownDescription: "API key for authentication. Can be set via MYCLOUD_API_KEY.",
            },
            "endpoint": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "API endpoint URL. Defaults to https://api.mycloud.com",
            },
            "region": schema.StringAttribute{
                Optional:            true,
                MarkdownDescription: "Default region for resources. Defaults to us-east-1",
            },
            "timeout": schema.NumberAttribute{
                Optional:            true,
                MarkdownDescription: "Request timeout in seconds. Defaults to 30.",
            },
        },
    }
}

Provider Configure

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    // Get configuration
    var config MyCloudProviderModel
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Resolve API key
    apiKey := config.APIKey.ValueString()
    if apiKey == "" {
        apiKey = os.Getenv("MYCLOUD_API_KEY")
    }
    
    if apiKey == "" {
        resp.Diagnostics.AddError(
            "Missing API Key",
            "API key must be provided via api_key attribute or MYCLOUD_API_KEY environment variable",
        )
        return
    }
    
    // Resolve endpoint
    endpoint := config.Endpoint.ValueString()
    if endpoint == "" {
        endpoint = os.Getenv("MYCLOUD_ENDPOINT")
    }
    if endpoint == "" {
        endpoint = "https://api.mycloud.com"
    }
    
    // Resolve region
    region := config.Region.ValueString()
    if region == "" {
        region = os.Getenv("MYCLOUD_REGION")
    }
    if region == "" {
        region = "us-east-1"
    }
    
    // Resolve timeout
    timeout := 30
    if !config.Timeout.IsNull() {
        timeoutFloat, _ := config.Timeout.ValueBigFloat().Int64()
        timeout = int(timeoutFloat)
    }
    
    // Create API client
    client, err := api.NewClient(api.Config{
        APIKey:   apiKey,
        BaseURL:  endpoint,
        Region:   region,
        Timeout:  time.Duration(timeout) * time.Second,
    })
    
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Create Client",
            fmt.Sprintf("Could not create API client: %s", err.Error()),
        )
        return
    }
    
    // Test connection
    if err := client.TestConnection(ctx); err != nil {
        resp.Diagnostics.AddError(
            "Connection Failed",
            fmt.Sprintf("Could not connect to API: %s", err.Error()),
        )
        return
    }
    
    // Share client with resources and data sources
    resp.ResourceData = client
    resp.DataSourceData = client
}

Provider Resources

func (p *MyCloudProvider) Resources(ctx context.Context) []func() resource.Resource {
    return []func() resource.Resource{
        NewEnvironmentResource,
        NewDatabaseResource,
        NewNetworkResource,
    }
}

func (p *MyCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
    return []func() datasource.DataSource{
        NewEnvironmentDataSource,
        NewDatabaseDataSource,
    }
}

Resource Implementation

Resource Struct with Configure

package provider

import (
    "context"
    "fmt"
    
    "github.com/hashicorp/terraform-plugin-framework/resource"
    "github.com/hashicorp/terraform-plugin-framework/resource/schema"
    "github.com/hashicorp/terraform-plugin-framework/types"
    
    "github.com/mycompany/mycloud-api-client/api"
)

type EnvironmentResource struct {
    client *api.Client
}

// Ensure resource implements interfaces
var _ resource.Resource = &EnvironmentResource{}
var _ resource.ResourceWithConfigure = &EnvironmentResource{}

func NewEnvironmentResource() resource.Resource {
    return &EnvironmentResource{}
}

Resource Configure Method

func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
    // Prevent panic if the provider has not been configured
    if req.ProviderData == nil {
        return
    }
    
    // Get client from provider
    client, ok := req.ProviderData.(*api.Client)
    if !ok {
        resp.Diagnostics.AddError(
            "Unexpected Resource Configure Type",
            fmt.Sprintf("Expected *api.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
        )
        return
    }
    
    // Store client for use in CRUD operations
    r.client = client
}

Using Shared Client in CRUD

func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var plan EnvironmentModel
    diags := req.Plan.Get(ctx, &plan)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // Use shared client
    env, err := r.client.CreateEnvironment(ctx, api.CreateEnvironmentRequest{
        Name:   plan.Name.ValueString(),
        Region: plan.Region.ValueString(),
    })
    
    if err != nil {
        resp.Diagnostics.AddError(
            "Error Creating Environment",
            fmt.Sprintf("Could not create environment: %s", err.Error()),
        )
        return
    }
    
    // Update state
    plan.ID = types.StringValue(env.ID)
    plan.ARN = types.StringValue(env.ARN)
    plan.Status = types.StringValue(env.Status)
    
    diags = resp.State.Set(ctx, plan)
    resp.Diagnostics.Append(diags...)
}

func (r *EnvironmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var state EnvironmentModel
    diags := req.State.Get(ctx, &state)
    resp.Diagnostics.Append(diags...)
    
    // Use shared client
    env, err := r.client.GetEnvironment(ctx, state.ID.ValueString())
    if err != nil {
        resp.Diagnostics.AddError("Read Error", err.Error())
        return
    }
    
    // Update state
    state.Status = types.StringValue(env.Status)
    state.UpdatedAt = types.StringValue(env.UpdatedAt)
    
    diags = resp.State.Set(ctx, state)
    resp.Diagnostics.Append(diags...)
}

Data Source Implementation

Data Source Struct

type EnvironmentDataSource struct {
    client *api.Client
}

var _ datasource.DataSource = &EnvironmentDataSource{}
var _ datasource.DataSourceWithConfigure = &EnvironmentDataSource{}

func NewEnvironmentDataSource() datasource.DataSource {
    return &EnvironmentDataSource{}
}

Data Source Configure

func (d *EnvironmentDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
    if req.ProviderData == nil {
        return
    }
    
    client, ok := req.ProviderData.(*api.Client)
    if !ok {
        resp.Diagnostics.AddError(
            "Unexpected Data Source Configure Type",
            fmt.Sprintf("Expected *api.Client, got: %T.", req.ProviderData),
        )
        return
    }
    
    d.client = client
}

Data Source Read

func (d *EnvironmentDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    var config EnvironmentDataSourceModel
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    
    // Use shared client
    env, err := d.client.GetEnvironmentByName(ctx, config.Name.ValueString())
    if err != nil {
        resp.Diagnostics.AddError("Read Error", err.Error())
        return
    }
    
    // Set data
    config.ID = types.StringValue(env.ID)
    config.ARN = types.StringValue(env.ARN)
    config.Status = types.StringValue(env.Status)
    
    diags = resp.State.Set(ctx, config)
    resp.Diagnostics.Append(diags...)
}

Error Handling

Check for Nil Client

func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    // Check if client is configured
    if r.client == nil {
        resp.Diagnostics.AddError(
            "Unconfigured Client",
            "Expected configured API client. Please report this issue to the provider developers.",
        )
        return
    }
    
    // Use client...
}

Handle Provider Configuration Errors

func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
    if req.ProviderData == nil {
        return
    }
    
    client, ok := req.ProviderData.(*api.Client)
    if !ok {
        resp.Diagnostics.AddError(
            "Unexpected Resource Configure Type",
            fmt.Sprintf("Expected *api.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
        )
        return
    }
    
    r.client = client
}

Testing Shared Clients

Unit Test with Mock Client

func TestEnvironmentResource_Create(t *testing.T) {
    // Create mock client
    mockClient := &api.MockClient{
        CreateEnvironmentFunc: func(ctx context.Context, req api.CreateEnvironmentRequest) (*api.Environment, error) {
            return &api.Environment{
                ID:     "env-123",
                Name:   req.Name,
                Region: req.Region,
                Status: "active",
            }, nil
        },
    }
    
    // Create resource with mock client
    resource := &EnvironmentResource{
        client: mockClient,
    }
    
    // Test Create operation
    // ...
}

Acceptance Test

func TestAccEnvironmentResource(t *testing.T) {
    resource.Test(t, resource.TestCase{
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        Steps: []resource.TestStep{
            {
                Config: testAccEnvironmentResourceConfig("test-env"),
                Check: resource.ComposeAggregateTestCheckFunc(
                    resource.TestCheckResourceAttr("mycloud_environment.test", "name", "test-env"),
                    resource.TestCheckResourceAttrSet("mycloud_environment.test", "id"),
                ),
            },
        },
    })
}

Advanced Patterns

Multiple Clients

If your provider needs multiple clients (e.g., different API versions):

type ClientBundle struct {
    V1Client *api.V1Client
    V2Client *api.V2Client
}

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    // ... configuration ...
    
    bundle := &ClientBundle{
        V1Client: api.NewV1Client(config),
        V2Client: api.NewV2Client(config),
    }
    
    resp.ResourceData = bundle
    resp.DataSourceData = bundle
}

func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
    bundle, ok := req.ProviderData.(*ClientBundle)
    if !ok {
        resp.Diagnostics.AddError("Unexpected Type", "...")
        return
    }
    
    r.v1Client = bundle.V1Client
    r.v2Client = bundle.V2Client
}

Client with Metadata

type ProviderClient struct {
    APIClient *api.Client
    Version   string
    UserAgent string
}

func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    // ... configuration ...
    
    providerClient := &ProviderClient{
        APIClient: client,
        Version:   p.version,
        UserAgent: fmt.Sprintf("terraform-provider-mycloud/%s", p.version),
    }
    
    resp.ResourceData = providerClient
    resp.DataSourceData = providerClient
}

Best Practices

1. Always Implement ResourceWithConfigure

// ✅ Good: Implement Configure interface
var _ resource.Resource = &EnvironmentResource{}
var _ resource.ResourceWithConfigure = &EnvironmentResource{}

2. Check for Nil Provider Data

// ✅ Good: Check for nil
func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
    if req.ProviderData == nil {
        return  // Provider not configured yet
    }
    // ...
}

3. Type Assert Safely

// ✅ Good: Safe type assertion with error
client, ok := req.ProviderData.(*api.Client)
if !ok {
    resp.Diagnostics.AddError("Unexpected Type", ...)
    return
}

4. Store Client in Resource Struct

// ✅ Good: Store for reuse
type EnvironmentResource struct {
    client *api.Client
}

func (r *EnvironmentResource) Configure(...) {
    r.client = client
}

5. Test Connection in Provider Configure

// ✅ Good: Test before sharing
if err := client.TestConnection(ctx); err != nil {
    resp.Diagnostics.AddError("Connection Failed", ...)
    return
}

Common Mistakes

❌ Creating New Client in Each Resource

// Bad: Creates new client every time
func (r *EnvironmentResource) Create(...) {
    client := api.NewClient(...)  // Don't do this!
}

❌ Not Checking for Nil

// Bad: Might panic if provider not configured
func (r *EnvironmentResource) Create(...) {
    env, err := r.client.CreateEnvironment(...)  // Panic if r.client is nil!
}

❌ Not Implementing Configure Interface

// Bad: Won't receive shared client
type EnvironmentResource struct {
    // Missing Configure method
}

Key Takeaways

  • ✅ Configure client once in provider’s Configure method
  • ✅ Share client via resp.ResourceData and resp.DataSourceData
  • ✅ Implement ResourceWithConfigure and DataSourceWithConfigure
  • ✅ Store client in resource/data source struct
  • ✅ Check for nil provider data in Configure
  • ✅ Use type assertions safely with error checking
  • ✅ Test connection before sharing client
  • ✅ Reuse HTTP connections for better performance
  • ✅ All resources/data sources use same configured client

What’s Next?

Now let’s learn about validating provider configuration.

Next: Configuration Validation →


Previous: ← Configuration Sources

Last updated: June 30, 2026

Updated: