# Configure the PokeAPI provider
Source: https://speakeasy.com/blog/create-a-terraform-provider-a-guide-for-beginners

This tutorial shows you how to create a simple Terraform provider for your web service. 

Terraform providers let your customers manage your API through declarative configuration files, the same way they manage AWS, Azure, or any other infrastructure. But HashiCorp's official tutorials are lengthy and overwhelming for developers new to provider development.

This guide cuts through the complexity, demonstrating how to build a working provider that implements create, read, update, and delete (CRUD) operations against a real API ([PokéAPI](https://pokeapi.co/)). You don't need any experience using Terraform to follow along — we'll explain everything as we go.

## Prerequisites

For this tutorial, you need the following installed on your machine:

- [Terraform](https://developer.hashicorp.com/terraform/install)
- [Go (1.21 or higher)](https://go.dev/doc/install)
- [Speakeasy CLI](https://www.speakeasy.com/docs/speakeasy-reference/cli/getting-started)

## Set up your system

First, clone the example project. 

```bash
git clone https://github.com/speakeasy-api/examples
cd examples/terraform-provider-pokeapi/base
```

The base project includes the OpenAPI document for the PokéAPI.

### Generate the PokéAPI SDK

Terraform providers require writing your own logic to create, read, update, and delete resources via API calls. Writing those HTTP client functions, request-response models, and error handling can take hundreds of lines or repetitive code. Instead, we'll use Speakeasy to generate a type-safe Go SDK that handles all API interactions for us.

First, install `staticcheck`, which is required by Speakeasy for linting:

```bash
go install honnef.co/go/tools/cmd/staticcheck@latest
export PATH=$PATH:$HOME/go/bin
```

To make this permanent, add the export to your shell profile (for example, `.bashrc` or `.zshrc`):

```bash
echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.zshrc
```

Generate the SDK from the OpenAPI document using the following command: 

```bash
speakeasy quickstart
```

Provide the following information as Speakeasy requests it:

- **The OpenAPI document location:** `openapi.yml`
- **Give your SDK a name:** `PokeAPISDK`
- **What you would like to generate:** `Software Development Kit (SDK)`
- **The SDK language you would like to generate:** `Go`
- **Choose a `modulePath`:** `github.com/pokeapi/sdk`
- **Choose a `sdkPackageName`:** `PokeAPISDK`
- **The directory the Go files should be written to:** `/path/to/project/poke-api-sdk`

This creates a complete SDK in the `poke-api-sdk` directory with built-in HTTP client logic, type-safe models, and error handling.

### Initialize the Go module

We're building a provider that connects Terraform to the PokéAPI. This requires two key dependencies.

- `terraform-plugin-framework`: The official library for building Terraform providers.
- `terraform-plugin-log`: The plugin for diagnostic logging and debugging.

Initialize the module following Terraform's naming convention:

```bash
go mod init example.com/me/terraform-provider-pokeapi
go get github.com/hashicorp/terraform-plugin-framework@latest
go get github.com/hashicorp/terraform-plugin-log@latest
```

Add the Speakeasy-generated SDK as a local dependency in `go.mod`:

```diff
// go.mod

module example.com/me/terraform-provider-pokeapi

go 1.24.0

toolchain go1.24.8

+ require (
+	github.com/pokeapi/sdk v0.0.0-00010101000000-000000000000
+)

require (
	github.com/hashicorp/terraform-plugin-framework v1.16.1 // indirect
	github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
)

+ replace github.com/pokeapi/sdk => ./poke-api-sdk
```

> **Note:** Adjust the path (`./poke-api-sdk`) to match the actual directory name Speakeasy created.

After editing `go.mod`, update the dependency tree:

```bash
go mod tidy
```

This command downloads all transitive dependencies and updates `go.sum` with checksums for the modules your provider needs.

## Create the provider files

So you have a web service, and in reality, you may even have an SDK in Python, Go, Java, and other languages that your customers could use to call your service. Why do you need Terraform, too?

We answer this question in detail in [our blog post about using Terraform as a SaaS API interface](https://www.speakeasy.com/post/build-terraform-providers). In summary, Terraform allows your customers to manage multiple environments with a single service (Terraform) through declarative configuration files that can be stored in Git. This means that if one of your customers wants to add a new user, or a whole new franchise, they can copy a Terraform resource configuration file from an existing franchise, update it, check it into GitHub, and get it approved. Then Terraform can run it automatically using continuous integration. Terraform provides benefits for your customers in terms of speed, safety, repeatability, auditing, and correctness.

A Terraform provider consists of three essential files that work together:

- An entry point that Terraform calls
- A provider configuration that sets up the API client
- Resource files that implement CRUD operations

### The entry point

Create a `main.go` file in your project root:

```go
package main

import (
    "context"
    "flag"
    "log"

    "example.com/me/terraform-provider-pokeapi/internal/provider"
    "github.com/hashicorp/terraform-plugin-framework/providerserver"
)

var (
    version string = "dev"
)

func main() {
    var debug bool

    flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
    flag.Parse()

    opts := providerserver.ServeOpts{
        Address: "example.com/me/pokeapi",
        Debug:   debug,
    }

    err := providerserver.Serve(context.Background(), provider.New(version), opts)

    if err != nil {
        log.Fatal(err.Error())
    }
}
```

This file starts the provider server that Terraform connects to. The `Address` is a unique identifier for your provider (like a package name). Users will reference this same address in their Terraform configurations to specify which provider they want to use. For example:

```hcl
terraform {
  required_providers {
    pokeapi = {
      source = "example.com/example/pokeapi"
    }
  }
}
```

### The provider configuration 

The provider configuration file configures the provider with the PokéAPI endpoint and initializes the Speakeasy SDK client.

Create an `internal/provider/provider.go` file:

```go
package provider

import (
    "context"

    "github.com/hashicorp/terraform-plugin-framework/datasource"
    "github.com/hashicorp/terraform-plugin-framework/function"
    "github.com/hashicorp/terraform-plugin-framework/path"
    "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"
    pokeapisdk "github.com/pokeapi/sdk"
)

var _ provider.Provider = &PokeAPIProvider{}
var _ provider.ProviderWithFunctions = &PokeAPIProvider{}

type PokeAPIProvider struct {
    version string
}

type PokeAPIProviderModel struct {
    Endpoint types.String `tfsdk:"endpoint"`
}

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

func (p *PokeAPIProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
    resp.Schema = schema.Schema{
        MarkdownDescription: `The PokeAPI provider allows you to manage Pokemon data using the Pokemon API.`,
        Attributes: map[string]schema.Attribute{
            "endpoint": schema.StringAttribute{
                MarkdownDescription: "PokeAPI endpoint URL. Defaults to https://pokeapi.co",
                Optional:            true,
            },
        },
    }
}

func (p *PokeAPIProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
    var data PokeAPIProviderModel

    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

    if resp.Diagnostics.HasError() {
        return
    }

    endpoint := "https://pokeapi.co"
    if !data.Endpoint.IsNull() {
        endpoint = data.Endpoint.ValueString()
    }

    if endpoint == "" {
        resp.Diagnostics.AddAttributeError(
            path.Root("endpoint"),
            "Missing PokeAPI Endpoint",
            "The provider cannot create the PokeAPI client as there is a missing or empty value for the PokeAPI endpoint.",
        )
    }

    if resp.Diagnostics.HasError() {
        return
    }

    client := pokeapisdk.New(
        pokeapisdk.WithServerURL(endpoint),
    )

    resp.DataSourceData = client
    resp.ResourceData = client
}

func (p *PokeAPIProvider) Resources(ctx context.Context) []func() resource.Resource {
    return []func() resource.Resource{
        NewPokemonResource,
    }
}

func (p *PokeAPIProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
    return []func() datasource.DataSource{}
}

func (p *PokeAPIProvider) Functions(ctx context.Context) []func() function.Function {
    return []func() function.Function{}
}

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

The Go file above implements the provider interface with several key functions:

- `Schema()` defines the provider's configuration schema with an optional `endpoint` attribute that users can set in their Terraform configs.
- `Configure()` initializes the Speakeasy SDK client with `pokeapisdk.New()`, instead of writing dozens of lines of HTTP client setup, and then stores it in `resp.ResourceData`.
- `Resources()` registers the `PokemonResource` so Terraform knows which resources this provider manages.
- `DataSources()` and `Functions()` return empty lists, since we're not implementing data sources or custom functions in this tutorial.

### The Pokémon resource

This is where the complexity begins to show – you must define schemas for every Pokémon attribute and implement CRUD operations. Large APIs with nested objects require hundreds of lines of repetitive schema definitions and type mappings.

Create a `internal/provider/pokemon_resource.go` file. Then, create the resources and schemas: 

```go
// internal/provider/pokemon_resource.go

package provider

import (
    "context"
    "fmt"

    "github.com/hashicorp/terraform-plugin-framework/attr"
    "github.com/hashicorp/terraform-plugin-framework/path"
    "github.com/hashicorp/terraform-plugin-framework/resource"
    "github.com/hashicorp/terraform-plugin-framework/resource/schema"
    "github.com/hashicorp/terraform-plugin-framework/types"

    pokeapisdk "github.com/pokeapi/sdk"
    "github.com/pokeapi/sdk/models/operations"
)

var _ resource.Resource = &PokemonResource{}
var _ resource.ResourceWithImportState = &PokemonResource{}

func NewPokemonResource() resource.Resource {
    return &PokemonResource{}
}

type PokemonResource struct {
    client *pokeapisdk.PokeApisdk
}

type PokemonResourceModel struct {
    ID             types.Int64  `tfsdk:"id"`
    Name           types.String `tfsdk:"name"`
    Height         types.Int64  `tfsdk:"height"`
    Weight         types.Int64  `tfsdk:"weight"`
    BaseExperience types.Int64  `tfsdk:"base_experience"`
    Stats          types.List   `tfsdk:"stats"`
    Types          types.List   `tfsdk:"types"`
    Abilities      types.List   `tfsdk:"abilities"`
    Sprites        types.Object `tfsdk:"sprites"`
}

func (r *PokemonResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
    resp.TypeName = req.ProviderTypeName + "_pokemon"
}

func (r *PokemonResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
    resp.Schema = schema.Schema{
        MarkdownDescription: "Pokemon resource",
        Attributes: map[string]schema.Attribute{
            "id": schema.Int64Attribute{
                MarkdownDescription: "Pokemon ID",
                Required:            true,
            },
            "name": schema.StringAttribute{
                MarkdownDescription: "Pokemon name",
                Computed:            true,
            },
            "height": schema.Int64Attribute{
                MarkdownDescription: "Height in decimetres",
                Computed:            true,
            },
            "weight": schema.Int64Attribute{
                MarkdownDescription: "Weight in hectograms",
                Computed:            true,
            },
            "base_experience": schema.Int64Attribute{
                MarkdownDescription: "Base experience",
                Computed:            true,
            },
            "stats": schema.ListNestedAttribute{
                MarkdownDescription: "Base stats",
                Computed:            true,
                NestedObject: schema.NestedAttributeObject{
                    Attributes: map[string]schema.Attribute{
                        "base_stat": schema.Int64Attribute{
                            Computed: true,
                        },
                        "effort": schema.Int64Attribute{
                            Computed: true,
                        },
                        "stat": schema.SingleNestedAttribute{
                            Computed: true,
                            Attributes: map[string]schema.Attribute{
                                "name": schema.StringAttribute{Computed: true},
                                "url":  schema.StringAttribute{Computed: true},
                            },
                        },
                    },
                },
            },
            "types": schema.ListNestedAttribute{
                MarkdownDescription: "Pokemon types",
                Computed:            true,
                NestedObject: schema.NestedAttributeObject{
                    Attributes: map[string]schema.Attribute{
                        "slot": schema.Int64Attribute{Computed: true},
                        "type": schema.SingleNestedAttribute{
                            Computed: true,
                            Attributes: map[string]schema.Attribute{
                                "name": schema.StringAttribute{Computed: true},
                                "url":  schema.StringAttribute{Computed: true},
                            },
                        },
                    },
                },
            },
            "abilities": schema.ListAttribute{
                MarkdownDescription: "Abilities",
                Computed:            true,
                ElementType: types.ObjectType{
                    AttrTypes: map[string]attr.Type{
                        "is_hidden": types.BoolType,
                        "slot":      types.Int64Type,
                        "ability": types.ObjectType{
                            AttrTypes: map[string]attr.Type{
                                "name": types.StringType,
                                "url":  types.StringType,
                            },
                        },
                    },
                },
            },
            "sprites": schema.SingleNestedAttribute{
                MarkdownDescription: "Sprite images",
                Computed:            true,
                Attributes: map[string]schema.Attribute{
                    "front_default": schema.StringAttribute{Computed: true},
                    "back_default":  schema.StringAttribute{Computed: true},
                },
            },
        },
    }
}

func (r *PokemonResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
    if req.ProviderData == nil {
        return
    }

    client, ok := req.ProviderData.(*pokeapisdk.PokeAPISDK)
    if !ok {
        resp.Diagnostics.AddError(
            "Unexpected Resource Configure Type",
            fmt.Sprintf("Expected *pokeapisdk.PokeAPISDK, got: %T", req.ProviderData),
        )
        return
    }

    r.client = client
}
```

The `PokemonResourceModel` struct uses Terraform-specific types (like `types.Int64` and `types.List`) instead of Go's native types, because Terraform needs to track null values and unknown states during planning. 

The `Schema()` function explicitly defines the structure of each attribute. Notice how nested objects like `stats` require verbose `NestedObject` definitions with their own attribute maps, making this one of the most tedious parts of provider development.

Now, add the CRUD operations:

```go
// internal/provider/pokemon_resource.go
...

func (r *PokemonResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
    var data PokemonResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    pokemonID := data.ID.ValueInt64()
    pokemon, err := r.client.Pokemon.PokemonRead(ctx, pokemonID)
    if err != nil {
        resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Pokemon: %s", err))
        return
    }

    if err := r.mapPokemonToState(ctx, pokemon, &data); err != nil {
        resp.Diagnostics.AddError("Mapping Error", fmt.Sprintf("Unable to map Pokemon: %s", err))
        return
    }

    resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *PokemonResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
    var data PokemonResourceModel
    resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    pokemonID := data.ID.ValueInt64()
    pokemon, err := r.client.Pokemon.PokemonRead(ctx, pokemonID)
    if err != nil {
        resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Pokemon: %s", err))
        return
    }

    if err := r.mapPokemonToState(ctx, pokemon, &data); err != nil {
        resp.Diagnostics.AddError("Mapping Error", fmt.Sprintf("Unable to map Pokemon: %s", err))
        return
    }

    resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *PokemonResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
    var data PokemonResourceModel
    resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    pokemonID := data.ID.ValueInt64()
    pokemon, err := r.client.Pokemon.PokemonRead(ctx, pokemonID)
    if err != nil {
        resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Pokemon: %s", err))
        return
    }

    if err := r.mapPokemonToState(ctx, pokemon, &data); err != nil {
        resp.Diagnostics.AddError("Mapping Error", fmt.Sprintf("Unable to map Pokemon: %s", err))
        return
    }

    resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *PokemonResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
    // Pokemon can't be deleted from the API, just remove from state
}

func (r *PokemonResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
    resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
```

Each CRUD function follows the same pattern:

- Get the current state with `req.Plan.Get()` or `req.State.Get()`.
- Call the API using the Speakeasy SDK (`r.client.Pokemon.PokemonRead()`).
- Map the response to Terraform types.
- Save the response with `resp.State.Set()`. 

Since PokéAPI is read-only, `Create()`, `Read()`, and `Update()` all fetch Pokémon data from the same endpoint, while `Delete()` is a no-op that just removes the resource from Terraform's state file.

Now, handle the data mapping:

```go
// internal/provider/pokemon_resource.go
...

func (r *PokemonResource) mapPokemonToState(ctx context.Context, pokemonResp *operations.PokemonReadResponse, data *PokemonResourceModel) error {
    if pokemonResp == nil || pokemonResp.Object == nil {
        return fmt.Errorf("received nil response from API")
    }

    pokemon := *pokemonResp.Object

    if pokemon.ID != nil {
        data.ID = types.Int64Value(int64(*pokemon.ID))
    }
    if pokemon.Name != nil {
        data.Name = types.StringValue(*pokemon.Name)
    }
    if pokemon.Height != nil {
        data.Height = types.Int64Value(int64(*pokemon.Height))
    }
    if pokemon.Weight != nil {
        data.Weight = types.Int64Value(int64(*pokemon.Weight))
    }
    if pokemon.BaseExperience != nil {
        data.BaseExperience = types.Int64Value(int64(*pokemon.BaseExperience))
    }

    // Map stats
    statsElements := []attr.Value{}
    if pokemon.Stats != nil {
        for _, stat := range pokemon.Stats {
            if stat.BaseStat == nil || stat.Effort == nil || stat.Stat.Name == nil || stat.Stat.URL == nil {
                continue
            }
            statObj, _ := types.ObjectValue(
                map[string]attr.Type{
                    "base_stat": types.Int64Type,
                    "effort":    types.Int64Type,
                    "stat": types.ObjectType{
                        AttrTypes: map[string]attr.Type{
                            "name": types.StringType,
                            "url":  types.StringType,
                        },
                    },
                },
                map[string]attr.Value{
                    "base_stat": types.Int64Value(*stat.BaseStat),
                    "effort":    types.Int64Value(*stat.Effort),
                    "stat": types.ObjectValueMust(
                        map[string]attr.Type{
                            "name": types.StringType,
                            "url":  types.StringType,
                        },
                        map[string]attr.Value{
                            "name": types.StringValue(*stat.Stat.Name),
                            "url":  types.StringValue(*stat.Stat.URL),
                        },
                    ),
                },
            )
            statsElements = append(statsElements, statObj)
        }
    }

    statsList, _ := types.ListValue(
        types.ObjectType{
            AttrTypes: map[string]attr.Type{
                "base_stat": types.Int64Type,
                "effort":    types.Int64Type,
                "stat": types.ObjectType{
                    AttrTypes: map[string]attr.Type{
                        "name": types.StringType,
                        "url":  types.StringType,
                    },
                },
            },
        },
        statsElements,
    )
    data.Stats = statsList

    // Map types
    typesElements := []attr.Value{}
    if pokemon.Types != nil {
        for _, typeInfo := range pokemon.Types {
            if typeInfo.Slot == nil || typeInfo.Type.Name == nil || typeInfo.Type.URL == nil {
                continue
            }
            typeObj, _ := types.ObjectValue(
                map[string]attr.Type{
                    "slot": types.Int64Type,
                    "type": types.ObjectType{
                        AttrTypes: map[string]attr.Type{
                            "name": types.StringType,
                            "url":  types.StringType,
                        },
                    },
                },
                map[string]attr.Value{
                    "slot": types.Int64Value(*typeInfo.Slot),
                    "type": types.ObjectValueMust(
                        map[string]attr.Type{
                            "name": types.StringType,
                            "url":  types.StringType,
                        },
                        map[string]attr.Value{
                            "name": types.StringValue(*typeInfo.Type.Name),
                            "url":  types.StringValue(*typeInfo.Type.URL),
                        },
                    ),
                },
            )
            typesElements = append(typesElements, typeObj)
        }
    }

    typesList, _ := types.ListValue(
        types.ObjectType{
            AttrTypes: map[string]attr.Type{
                "slot": types.Int64Type,
                "type": types.ObjectType{
                    AttrTypes: map[string]attr.Type{
                        "name": types.StringType,
                        "url":  types.StringType,
                    },
                },
            },
        },
        typesElements,
    )
    data.Types = typesList

    // Create empty collections for other fields
    abilitiesEmpty, _ := types.ListValue(
        types.ObjectType{
            AttrTypes: map[string]attr.Type{
                "is_hidden": types.BoolType,
                "slot":      types.Int64Type,
                "ability": types.ObjectType{
                    AttrTypes: map[string]attr.Type{
                        "name": types.StringType,
                        "url":  types.StringType,
                    },
                },
            },
        },
        []attr.Value{},
    )
    data.Abilities = abilitiesEmpty

    spritesEmpty, _ := types.ObjectValue(
        map[string]attr.Type{
            "front_default": types.StringType,
            "back_default":  types.StringType,
        },
        map[string]attr.Value{
            "front_default": types.StringNull(),
            "back_default":  types.StringNull(),
        },
    )
    data.Sprites = spritesEmpty

    return nil
}
```

The `mapPokemonToState()` function converts the SDK's response types to the Terraform type system.

For each nested list like `stats`, you need to:

- Define the exact `AttrTypes` structure that matches your schema
- Iterate through the API response, checking for nil values
- Construct properly typed objects using `types.ObjectValue()`

In this manual, repetitive mapping consumes most of the provider development time, and any mismatch between the `AttrTypes` map here and the schema definition can cause cryptic runtime type errors.

## Run the provider

At this point, you have:

- Generated an SDK with Speakeasy to handle API calls to PokéAPI
- Created three provider files, an entry point (main.go), a provider configuration (`provider.go`), and a Pokémon resource with CRUD operations (`pokemon_resource.go`)
- Implemented complex data mapping that links API responses to Terraform state

Now it's time to test the provider by pretending you're a user who wants to manage Pokémon data through Terraform.

### Configure local development

Because you haven't published your provider to the Terraform registry yet, you need to tell Terraform to use your local build. Create a `.terraformrc` file in your home directory:

```bash
cat > ~/.terraformrc << 'EOF'
provider_installation {
    dev_overrides {
        "example.com/me/pokeapi" = "/absolute/path/to/your/project/bin"
    }
    direct {} # For all other providers, install directly from their origin provider.
}
EOF
```

The `dev_overrides` setting tells Terraform to use your local binary instead of downloading from the registry.

Replace `/absolute/path/to/your/project/bin` with the actual absolute path to your project's bin directory. For example: `/Users/yourusername/terraform-provider-pokeapi/bin`.

### Build the server

Compile the provider into an executable binary:

```bash
go build -o ./bin/terraform-provider-pokeapi
```

The command above creates the provider plugin that Terraform will call to manage Pokémon resources.

### Create a test configuration

Create an `examples` directory to hold your Terraform configuration:

```bash
mkdir -p examples
cd examples
```

Create a `main.tf` file inside the `examples` directory. This is what a user of your provider would write:

```tf

terraform {
  required_providers {
    pokeapi = {
      source = "example.com/me/pokeapi"
    }
  }
}

provider "pokeapi" {
  endpoint = "https://pokeapi.co"
}

resource "pokeapi_pokemon" "pikachu" {
  id = 25 # Pikachu's ID
}

resource "pokeapi_pokemon" "charizard" {
  id = 6 # Charizard's ID
}

output "pikachu_name" {
  description = "The name of Pikachu"
  value       = pokeapi_pokemon.pikachu.name
}

output "pikachu_height" {
  description = "Pikachu's height in decimetres"
  value       = pokeapi_pokemon.pikachu.height
}

output "pikachu_weight" {
  description = "Pikachu's weight in hectograms"
  value       = pokeapi_pokemon.pikachu.weight
}

output "pikachu_base_experience" {
  description = "Base experience gained from defeating Pikachu"
  value       = pokeapi_pokemon.pikachu.base_experience
}

output "pikachu_stats" {
  description = "Pikachu's base stats - this is a complex nested list"
  value       = pokeapi_pokemon.pikachu.stats
}

output "pikachu_types" {
  description = "Pikachu's types - this is a nested list with slot and type information"
  value       = pokeapi_pokemon.pikachu.types
}

output "charizard_name" {
  description = "Charizard's name"
  value       = pokeapi_pokemon.charizard.name
}
```

This configuration tells Terraform to fetch data for two Pokémon (Pikachu and Charizard) and output some of their attributes.

### Apply the configuration

Run Terraform to execute the configuration:

```bash
terraform plan    # Preview what Terraform will do
terraform apply -auto-approve  # Apply the changes
```

Terraform should output the following:

```txt
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # pokeapi_pokemon.charizard will be created
  + resource "pokeapi_pokemon" "charizard" {
      + abilities                = (known after apply)
      + base_experience          = (known after apply)
      + cries                    = (known after apply)
      + game_indices             = (known after apply)
      + height                   = (known after apply)
      + held_items               = (known after apply)
      + id                       = 6
      + is_default               = (known after apply)
      + location_area_encounters = (known after apply)
      + moves                    = (known after apply)
      + name                     = (known after apply)
      + order                    = (known after apply)
      + past_abilities           = (known after apply)
      + past_types               = (known after apply)
      + species                  = (known after apply)
      + sprites                  = (known after apply)
      + stats                    = (known after apply)
      + types                    = (known after apply)
      + weight                   = (known after apply)
    }

  # pokeapi_pokemon.pikachu will be created
  + resource "pokeapi_pokemon" "pikachu" {
      + abilities                = (known after apply)
      + base_experience          = (known after apply)
      + cries                    = (known after apply)
      + game_indices             = (known after apply)
      + height                   = (known after apply)
      + held_items               = (known after apply)
      + id                       = 25
      + is_default               = (known after apply)
      + location_area_encounters = (known after apply)
      + moves                    = (known after apply)
      + name                     = (known after apply)
      + order                    = (known after apply)
      + past_abilities           = (known after apply)
      + past_types               = (known after apply)
      + species                  = (known after apply)
      + sprites                  = (known after apply)
      + stats                    = (known after apply)
      + types                    = (known after apply)
      + weight                   = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + charizard_name          = (known after apply)
  + pikachu_base_experience = (known after apply)
  + pikachu_height          = (known after apply)
  + pikachu_name            = (known after apply)
  + pikachu_stats           = (known after apply)
  + pikachu_types           = (known after apply)
  + pikachu_weight          = (known after apply)
pokeapi_pokemon.pikachu: Creating...
pokeapi_pokemon.charizard: Creating...
pokeapi_pokemon.pikachu: Creation complete after 0s [name=pikachu]
pokeapi_pokemon.charizard: Creation complete after 0s [name=charizard]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

charizard_name = "charizard"
pikachu_base_experience = 112
pikachu_height = 4
pikachu_name = "pikachu"
pikachu_stats = tolist([
  {
    "base_stat" = 35
    "effort" = 0
    "stat" = {
      "name" = "hp"
      "url" = "https://pokeapi.co/api/v2/stat/1/"
    }
  },
  {
    "base_stat" = 55
    "effort" = 0
    "stat" = {
      "name" = "attack"
      "url" = "https://pokeapi.co/api/v2/stat/2/"
    }
  },
  {
    "base_stat" = 40
    "effort" = 0
    "stat" = {
      "name" = "defense"
      "url" = "https://pokeapi.co/api/v2/stat/3/"
    }
  },
  {
    "base_stat" = 50
    "effort" = 0
    "stat" = {
      "name" = "special-attack"
      "url" = "https://pokeapi.co/api/v2/stat/4/"
    }
  },
  {
    "base_stat" = 50
    "effort" = 0
    "stat" = {
      "name" = "special-defense"
      "url" = "https://pokeapi.co/api/v2/stat/5/"
    }
  },
  {
    "base_stat" = 90
    "effort" = 2
    "stat" = {
      "name" = "speed"
      "url" = "https://pokeapi.co/api/v2/stat/6/"
    }
  },
])
pikachu_types = tolist([
  {
    "slot" = 1
    "type" = {
      "name" = "electric"
      "url" = "https://pokeapi.co/api/v2/type/13/"
    }
  },
])
pikachu_weight = 60
```

Terraform called your provider's `Create()` function for each Pokemon resource, which:

- Made an API call to PokéAPI using the Speakeasy SDK
- Mapped the API response to Terraform's type system
- Stored the Pokemon data in `terraform.tfstate`

Check the state file `terraform.tfstate` that was created. It will look similar to the following example:

```txt  
{
  "version": 4,
  "terraform_version": "1.9.5",
  "serial": 3,
  "lineage": "317ebf1d-403e-03fa-787e-4694386c5acd",
  "outputs": {
    "charizard_name": {
      "value": "charizard",
      "type": "string"
    },
    "pikachu_abilities": {
      "value": [],
      "type": [
        "list",
        [
          "object",
...
}
```

This file contains Terraform's view of the Pokémon resources. Never edit the state file manually; always let Terraform manage it.

### Debugging tips

If you need to debug the provider, enable logging:

```bash
export TF_LOG=DEBUG
terraform apply
```

You can also add logging in your provider code:

```go
import "github.com/hashicorp/terraform-plugin-log/tflog"

// Inside any function
tflog.Info(ctx, "Reading Pokemon", map[string]any{"id": pokemonID})
```

To push the current project a bit more, try modifying your configuration to see how Terraform handles changes:

- Add more Pokémon resources with different IDs.
- Run `terraform plan` to see what would change.
- Run `terraform destroy` to remove resources from state.

## Limitations and further reading

You now have a working minimal example of a Terraform provider, but it isn't ready for production use yet. We recommend first enhancing it with features such as the following:

- **Markup responses (JSON or XML):** PokéAPI returns JSON, which simplified our implementation. However, your API might return XML, Protobuf, or custom formats that require additional parsing logic. You need to handle serialization, deserialization, and error responses for the format your service uses.
- **Versioning and continuous integration:** Your web service will change over time, and the provider needs to change to match it. Your customers will need to use the correct versions of both the web service and the provider. We also recommend automatically building and releasing your provider from GitHub, using GitHub Actions.
- **Testing:** A real web service is complex, and you need to write a lot of integration tests to ensure that every provider version you release does exactly what it's supposed to when calling the service.
- **Documentation:** Your customers want to know exactly how to set up and configure your provider to manage whatever resources your service offers.
- **Publishing the provider to the Terraform registry:** Until you add metadata to your provider and release it in the Terraform ecosystem, no one can use it.

You can also add additional functionality, like handling data sources (which are different from resources) and external imports of resources.

If you want to learn how to enhance your provider, the best place to start is the official [Terraform provider creation tutorial](https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework/providers-plugin-framework-provider). You can also clone the [provider scaffolding repository](https://github.com/hashicorp/terraform-provider-scaffolding-framework) and read through it to see how Terraform structures a provider and uses `.github` to offer continuous integration.

Once you've worked through the tutorial, we recommend reading about [how Terraform works with plugins](https://developer.hashicorp.com/terraform/plugin/how-terraform-works). 

HashiCorp released an [automated provider generator](https://developer.hashicorp.com/terraform/plugin/code-generation) in technical preview during 2024, but development has stalled, and it doesn't handle API operation logic or data type conversions - you still write most code manually. We cover this tool and production-ready alternatives in our article on [building Terraform providers](https://www.speakeasy.com/blog/how-to-build-terraform-providers).

## A simpler way

You might feel that creating and maintaining your own Terraform provider is far too much work when you're busy trying to run a business and provide your core service. Luckily, there is a much easier alternative. We at Speakeasy are passionate about and dedicated to making web APIs easy for customers to use. Our service can automatically generate a complete Terraform provider with documentation that's ready for you to offer to your customers on the Terraform registry. All you need is an OpenAPI document for your service and a few custom attributes.

Read our [guide to generating a Terraform provider with Speakeasy](https://www.speakeasy.com/blog/how-to-build-terraform-providers) and see how we can massively reduce your workload by setting up a Terraform provider in a few clicks.