Powered by AppSignal & Oban Pro

STAC API Examples with Req

misc/stac_api_examples.livemd

STAC API Examples with Req

Mix.install([
  {:req, "~> 0.5"}
])

Setup

Configure your STAC API base URL and API key:

base_url = "http://localhost:4000"
api_version = "/stac/api/v1"
manage_version = "/stac/manage/v1"

read_write_api_key = System.get_env("STAC_API_KEY") || "dev-api-key-2024"
read_only_api_key = System.get_env("STAC_API_KEY_RO") || "dev-read-only-key-2024"

# STAC API read client — public endpoints, no auth required
# Pass X-API-Key here to unlock private catalogs/collections/items
req = Req.new(base_url: base_url <> api_version)

# Management API client — all CRUD operations require the RW key
manage_req = Req.new(
  base_url: base_url <> manage_version,
  headers: [{"x-api-key", read_write_api_key}]
)
%Req.Request{
  method: :get,
  url: URI.parse(""),
  headers: %{},
  body: nil,
  options: %{base_url: "http://localhost:4000/stac/api/v1"},
  halted: false,
  adapter: &Req.Steps.run_finch/1,
  request_steps: [
    put_user_agent: &Req.Steps.put_user_agent/1,
    compressed: &Req.Steps.compressed/1,
    encode_body: &Req.Steps.encode_body/1,
    put_base_url: &Req.Steps.put_base_url/1,
    auth: &Req.Steps.auth/1,
    put_params: &Req.Steps.put_params/1,
    put_path_params: &Req.Steps.put_path_params/1,
    put_range: &Req.Steps.put_range/1,
    cache: &Req.Steps.cache/1,
    put_plug: &Req.Steps.put_plug/1,
    compress_body: &Req.Steps.compress_body/1,
    checksum: &Req.Steps.checksum/1,
    put_aws_sigv4: &Req.Steps.put_aws_sigv4/1
  ],
  response_steps: [
    retry: &Req.Steps.retry/1,
    handle_http_errors: &Req.Steps.handle_http_errors/1,
    redirect: &Req.Steps.redirect/1,
    http_digest: &Req.Steps.handle_http_digest/1,
    decompress_body: &Req.Steps.decompress_body/1,
    verify_checksum: &Req.Steps.verify_checksum/1,
    decode_body: &Req.Steps.decode_body/1,
    output: &Req.Steps.output/1
  ],
  error_steps: [retry: &Req.Steps.retry/1],
  private: %{}
}
read_only_api_key
"dev-read-only-key-2024"

Example 1: Exploring Catalogs

Catalogs are managed via the Management API (/stac/manage/v1) and always require authentication. Collections and items can also be browsed via the public STAC API (/stac/api/v1).

# Get all catalogs (management endpoint — requires RW key)
{:ok, response} = Req.get(manage_req, url: "/catalogs")

IO.puts("Status: #{response.status}")
IO.puts("\nCatalogs:")
IO.inspect(response.body["catalogs"], pretty: true)
Status: 200

Catalogs:
[]
[]
# Get a specific catalog by ID (management endpoint)
{:ok, response} = Req.get(manage_req, url: "/catalogs/my-test-catalog")

IO.puts("Catalog Details:")
IO.inspect(response.body, pretty: true)
Catalog Details:
%{
  "description" => "A catalog for testing purposes",
  "extent" => nil,
  "id" => "my-test-catalog",
  "links" => [
    %{
      "href" => "/stac/api/v1/catalog/my-test-catalog",
      "rel" => "self",
      "type" => "application/json"
    },
    %{"href" => "/stac/api/v1/", "rel" => "root", "type" => "application/json"},
    %{
      "href" => "/stac/api/v1/",
      "rel" => "parent",
      "type" => "application/json"
    },
    %{
      "href" => "https://creativecommons.org/licenses/by/4.0/",
      "rel" => "license",
      "title" => "CC BY 4.0"
    }
  ],
  "stac_version" => "1.0.0",
  "title" => "My Test Catalog",
  "type" => "Catalog"
}
%{
  "description" => "A catalog for testing purposes",
  "extent" => nil,
  "id" => "my-test-catalog",
  "links" => [
    %{
      "href" => "/stac/api/v1/catalog/my-test-catalog",
      "rel" => "self",
      "type" => "application/json"
    },
    %{"href" => "/stac/api/v1/", "rel" => "root", "type" => "application/json"},
    %{"href" => "/stac/api/v1/", "rel" => "parent", "type" => "application/json"},
    %{
      "href" => "https://creativecommons.org/licenses/by/4.0/",
      "rel" => "license",
      "title" => "CC BY 4.0"
    }
  ],
  "stac_version" => "1.0.0",
  "title" => "My Test Catalog",
  "type" => "Catalog"
}
# Get the root API landing page
{:ok, response} = Req.get(req, url: "/")

IO.puts("Root API:")
IO.inspect(response.body, pretty: true)
Root API:
%{
  "conformsTo" => ["https://api.stacspec.org/v1.0.0/core",
   "https://api.stacspec.org/v1.0.0/item-search"],
  "description" => "SpatioTemporal Asset Catalog API for geospatial data discovery and access",
  "id" => "geokuup-stac-api",
  "links" => [
    %{
      "href" => "http://localhost:4000/stac/api/v1/",
      "rel" => "self",
      "type" => "application/json"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/",
      "rel" => "root",
      "type" => "application/json"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/openapi.json",
      "rel" => "service-desc",
      "title" => "OpenAPI service description",
      "type" => "application/vnd.oai.openapi+json;version=3.0"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/docs",
      "rel" => "service-doc",
      "title" => "OpenAPI service documentation",
      "type" => "text/html"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/collections",
      "rel" => "data",
      "title" => "Collections",
      "type" => "application/json"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/search",
      "method" => "GET",
      "rel" => "search",
      "title" => "STAC search",
      "type" => "application/geo+json"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/search",
      "method" => "POST",
      "rel" => "search",
      "title" => "STAC search",
      "type" => "application/geo+json"
    },
    %{
      "href" => "http://localhost:4000/stac/web/browse",
      "rel" => "browser",
      "title" => "Web Browser Interface",
      "type" => "text/html"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/catalog/my-test-catalog",
      "rel" => "child",
      "title" => "My Test Catalog",
      "type" => "application/json"
    }
  ],
  "stac_extensions" => [],
  "stac_version" => "1.0.0",
  "title" => "Geokuup STAC API",
  "type" => "Catalog"
}
%{
  "conformsTo" => ["https://api.stacspec.org/v1.0.0/core",
   "https://api.stacspec.org/v1.0.0/item-search"],
  "description" => "SpatioTemporal Asset Catalog API for geospatial data discovery and access",
  "id" => "geokuup-stac-api",
  "links" => [
    %{"href" => "http://localhost:4000/stac/api/v1/", "rel" => "self", "type" => "application/json"},
    %{"href" => "http://localhost:4000/stac/api/v1/", "rel" => "root", "type" => "application/json"},
    %{
      "href" => "http://localhost:4000/stac/api/v1/openapi.json",
      "rel" => "service-desc",
      "title" => "OpenAPI service description",
      "type" => "application/vnd.oai.openapi+json;version=3.0"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/docs",
      "rel" => "service-doc",
      "title" => "OpenAPI service documentation",
      "type" => "text/html"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/collections",
      "rel" => "data",
      "title" => "Collections",
      "type" => "application/json"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/search",
      "method" => "GET",
      "rel" => "search",
      "title" => "STAC search",
      "type" => "application/geo+json"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/search",
      "method" => "POST",
      "rel" => "search",
      "title" => "STAC search",
      "type" => "application/geo+json"
    },
    %{
      "href" => "http://localhost:4000/stac/web/browse",
      "rel" => "browser",
      "title" => "Web Browser Interface",
      "type" => "text/html"
    },
    %{
      "href" => "http://localhost:4000/stac/api/v1/catalog/my-test-catalog",
      "rel" => "child",
      "title" => "My Test Catalog",
      "type" => "application/json"
    }
  ],
  "stac_extensions" => [],
  "stac_version" => "1.0.0",
  "title" => "Geokuup STAC API",
  "type" => "Catalog"
}

Example 2: Creating Resources (With Auth)

Creating, updating, and deleting require the RW X-API-Key header and go through the Management API (/stac/manage/v1). Use manage_req defined in Setup, or:

# Equivalent to manage_req — shown explicitly for clarity
rw_auth_req = Req.new(
  base_url: base_url <> manage_version,
  headers: [{"x-api-key", read_write_api_key}]
)
%Req.Request{
  method: :get,
  url: URI.parse(""),
  headers: %{"x-api-key" => ["dev-api-key-2024"]},
  body: nil,
  options: %{base_url: "http://localhost:4000/stac/api/v1"},
  halted: false,
  adapter: &Req.Steps.run_finch/1,
  request_steps: [
    put_user_agent: &Req.Steps.put_user_agent/1,
    compressed: &Req.Steps.compressed/1,
    encode_body: &Req.Steps.encode_body/1,
    put_base_url: &Req.Steps.put_base_url/1,
    auth: &Req.Steps.auth/1,
    put_params: &Req.Steps.put_params/1,
    put_path_params: &Req.Steps.put_path_params/1,
    put_range: &Req.Steps.put_range/1,
    cache: &Req.Steps.cache/1,
    put_plug: &Req.Steps.put_plug/1,
    compress_body: &Req.Steps.compress_body/1,
    checksum: &Req.Steps.checksum/1,
    put_aws_sigv4: &Req.Steps.put_aws_sigv4/1
  ],
  response_steps: [
    retry: &Req.Steps.retry/1,
    handle_http_errors: &Req.Steps.handle_http_errors/1,
    redirect: &Req.Steps.redirect/1,
    http_digest: &Req.Steps.handle_http_digest/1,
    decompress_body: &Req.Steps.decompress_body/1,
    verify_checksum: &Req.Steps.verify_checksum/1,
    decode_body: &Req.Steps.decode_body/1,
    output: &Req.Steps.output/1
  ],
  error_steps: [retry: &Req.Steps.retry/1],
  private: %{}
}

Create a Root Catalog

catalog_data = %{
  "id" => "my-test-catalog",
  "title" => "My Test Catalog",
  "description" => "A catalog for testing purposes",
  "type" => "Catalog",
  "stac_version" => "1.0.0",
  "links" => [
    %{
      "rel" => "license",
      "href" => "https://creativecommons.org/licenses/by/4.0/",
      "title" => "CC BY 4.0"
    }
  ]
}

{:ok, response} = Req.post(rw_auth_req, url: "/catalogs", json: catalog_data)

IO.puts("Status: #{response.status}")
IO.inspect(response.body, pretty: true)
Status: 409
%{"error" => "Catalog with ID 'my-test-catalog' already exists"}
%{"error" => "Catalog with ID 'my-test-catalog' already exists"}

Create a Nested Catalog

nested_catalog_data = %{
  "id" => "my-nested-catalog",
  "title" => "My Nested Catalog",
  "description" => "A nested catalog under my-test-catalog",
  "type" => "Catalog",
  "stac_version" => "1.0.0",
  "parent_catalog_id" => "my-test-catalog"
}

{:ok, response} = Req.post(rw_auth_req, url: "/catalogs", json: nested_catalog_data)

IO.puts("Status: #{response.status}")
IO.inspect(response.body, pretty: true)
Status: 201
%{
  "data" => %{
    "description" => "A nested catalog under my-test-catalog",
    "extent" => nil,
    "id" => "my-nested-catalog",
    "links" => [
      %{
        "href" => "/stac/api/v1/catalog/my-nested-catalog",
        "rel" => "self",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/",
        "rel" => "root",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "parent",
        "type" => "application/json"
      }
    ],
    "stac_version" => "1.0.0",
    "title" => "My Nested Catalog",
    "type" => "Catalog"
  },
  "message" => "Catalog created successfully",
  "success" => true
}
%{
  "data" => %{
    "description" => "A nested catalog under my-test-catalog",
    "extent" => nil,
    "id" => "my-nested-catalog",
    "links" => [
      %{
        "href" => "/stac/api/v1/catalog/my-nested-catalog",
        "rel" => "self",
        "type" => "application/json"
      },
      %{"href" => "/stac/api/v1/", "rel" => "root", "type" => "application/json"},
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "parent",
        "type" => "application/json"
      }
    ],
    "stac_version" => "1.0.0",
    "title" => "My Nested Catalog",
    "type" => "Catalog"
  },
  "message" => "Catalog created successfully",
  "success" => true
}

Create a Collection

collection_data = %{
  "id" => "my-test-collection",
  "title" => "My Test Collection",
  "description" => "A collection for testing",
  "license" => "CC-BY-4.0",
  "catalog_id" => "my-test-catalog",
  "stac_version" => "1.0.0",
  "extent" => %{
    "spatial" => %{
      "bbox" => [[-180, -90, 180, 90]]
    },
    "temporal" => %{
      "interval" => [["2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z"]]
    }
  }
}

{:ok, response} = Req.post(rw_auth_req, url: "/collections", json: collection_data)

IO.puts("Status: #{response.status}")
IO.inspect(response.body, pretty: true)
Status: 201
%{
  "data" => %{
    "description" => "A collection for testing",
    "extent" => %{
      "spatial" => %{"bbox" => [[-180, -90, 180, 90]]},
      "temporal" => %{
        "interval" => [["2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z"]]
      }
    },
    "id" => "my-test-collection",
    "license" => "CC-BY-4.0",
    "links" => [
      %{
        "href" => "/stac/api/v1/collections/my-test-collection",
        "rel" => "self",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/",
        "rel" => "root",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/collections/my-test-collection/items",
        "rel" => "items",
        "type" => "application/geo+json"
      },
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "parent",
        "type" => "application/json"
      }
    ],
    "properties" => %{},
    "stac_extensions" => [],
    "stac_version" => "1.0.0",
    "summaries" => nil,
    "title" => "My Test Collection",
    "type" => "Collection"
  },
  "message" => "Collection created successfully",
  "success" => true
}
%{
  "data" => %{
    "description" => "A collection for testing",
    "extent" => %{
      "spatial" => %{"bbox" => [[-180, -90, 180, 90]]},
      "temporal" => %{"interval" => [["2024-01-01T00:00:00Z", "2024-12-31T23:59:59Z"]]}
    },
    "id" => "my-test-collection",
    "license" => "CC-BY-4.0",
    "links" => [
      %{
        "href" => "/stac/api/v1/collections/my-test-collection",
        "rel" => "self",
        "type" => "application/json"
      },
      %{"href" => "/stac/api/v1/", "rel" => "root", "type" => "application/json"},
      %{
        "href" => "/stac/api/v1/collections/my-test-collection/items",
        "rel" => "items",
        "type" => "application/geo+json"
      },
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "parent",
        "type" => "application/json"
      }
    ],
    "properties" => %{},
    "stac_extensions" => [],
    "stac_version" => "1.0.0",
    "summaries" => nil,
    "title" => "My Test Collection",
    "type" => "Collection"
  },
  "message" => "Collection created successfully",
  "success" => true
}

Example 3: Updating Resources (With Auth)

Full Update (PUT)

updated_catalog_data = %{
  "id" => "my-test-catalog",
  "title" => "My Updated Test Catalog",
  "description" => "This catalog has been updated with new information",
  "type" => "Catalog",
  "stac_version" => "1.0.0"
}

{:ok, response} = Req.put(rw_auth_req, 
  url: "/catalogs/my-test-catalog", 
  json: updated_catalog_data
)

IO.puts("Status: #{response.status}")
IO.inspect(response.body, pretty: true)
Status: 200
%{
  "data" => %{
    "description" => "This catalog has been updated with new information",
    "extent" => nil,
    "id" => "my-test-catalog",
    "links" => [
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "self",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/",
        "rel" => "root",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/",
        "rel" => "parent",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/catalog/my-nested-catalog",
        "rel" => "child",
        "title" => "My Nested Catalog",
        "type" => "application/json"
      }
    ],
    "stac_version" => "1.0.0",
    "title" => "My Updated Test Catalog",
    "type" => "Catalog"
  },
  "message" => "Catalog replaced successfully",
  "success" => true
}
%{
  "data" => %{
    "description" => "This catalog has been updated with new information",
    "extent" => nil,
    "id" => "my-test-catalog",
    "links" => [
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "self",
        "type" => "application/json"
      },
      %{"href" => "/stac/api/v1/", "rel" => "root", "type" => "application/json"},
      %{"href" => "/stac/api/v1/", "rel" => "parent", "type" => "application/json"},
      %{
        "href" => "/stac/api/v1/catalog/my-nested-catalog",
        "rel" => "child",
        "title" => "My Nested Catalog",
        "type" => "application/json"
      }
    ],
    "stac_version" => "1.0.0",
    "title" => "My Updated Test Catalog",
    "type" => "Catalog"
  },
  "message" => "Catalog replaced successfully",
  "success" => true
}

Partial Update (PATCH)

# Only update the title, keep everything else
patch_data = %{
  "id" => "my-test-catalog",
  "title" => "Partially Updated Catalog"
}

{:ok, response} = Req.patch(rw_auth_req, 
  url: "/catalogs/my-test-catalog", 
  json: patch_data
)

IO.puts("Status: #{response.status}")
IO.inspect(response.body, pretty: true)
Status: 200
%{
  "data" => %{
    "description" => "This catalog has been updated with new information",
    "extent" => nil,
    "id" => "my-test-catalog",
    "links" => [
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "self",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/",
        "rel" => "root",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/",
        "rel" => "parent",
        "type" => "application/json"
      },
      %{
        "href" => "/stac/api/v1/catalog/my-nested-catalog",
        "rel" => "child",
        "title" => "My Nested Catalog",
        "type" => "application/json"
      }
    ],
    "stac_version" => "1.0.0",
    "title" => "Partially Updated Catalog",
    "type" => "Catalog"
  },
  "message" => "Catalog updated successfully",
  "success" => true
}
%{
  "data" => %{
    "description" => "This catalog has been updated with new information",
    "extent" => nil,
    "id" => "my-test-catalog",
    "links" => [
      %{
        "href" => "/stac/api/v1/catalog/my-test-catalog",
        "rel" => "self",
        "type" => "application/json"
      },
      %{"href" => "/stac/api/v1/", "rel" => "root", "type" => "application/json"},
      %{"href" => "/stac/api/v1/", "rel" => "parent", "type" => "application/json"},
      %{
        "href" => "/stac/api/v1/catalog/my-nested-catalog",
        "rel" => "child",
        "title" => "My Nested Catalog",
        "type" => "application/json"
      }
    ],
    "stac_version" => "1.0.0",
    "title" => "Partially Updated Catalog",
    "type" => "Catalog"
  },
  "message" => "Catalog updated successfully",
  "success" => true
}

Example 4: Deleting Resources (With Auth)

⚠️ Warning: Deleting a catalog will CASCADE delete all its children (nested catalogs, collections, and items)!

Delete a Specific Catalog

# Delete the nested catalog first (safer)
{:ok, response} = Req.delete(rw_auth_req, url: "/catalogs/my-nested-catalog")

IO.puts("Status: #{response.status}")
IO.inspect(response.body, pretty: true)
Status: 200
%{
  "cascade_deleted" => %{"catalogs" => 0, "collections" => 0, "items" => 0},
  "message" => "Catalog deleted successfully",
  "success" => true
}
%{
  "cascade_deleted" => %{"catalogs" => 0, "collections" => 0, "items" => 0},
  "message" => "Catalog deleted successfully",
  "success" => true
}

Delete with Cascade

# This will delete the catalog AND all its children
{:ok, response} = Req.delete(manage_req, url: "/catalogs/my-test-catalog")

IO.puts("Status: #{response.status}")
IO.puts("\nCascade Delete Summary:")
IO.inspect(response.body["cascade_deleted"], pretty: true)

Complete Workflow Example

Here’s a complete example showing the full lifecycle:

# 1. Create a demo hierarchy
IO.puts("1. Creating demo catalog...")
demo_catalog = %{
  "id" => "demo-catalog-#{:rand.uniform(10000)}",
  "title" => "Demo Catalog",
  "description" => "Temporary demo catalog"
}

{:ok, catalog_response} = Req.post(manage_req, url: "/catalogs", json: demo_catalog)
catalog_id = catalog_response.body["data"]["id"]
IO.puts("   Created catalog: #{catalog_id}")

# 2. Create a collection under it
IO.puts("\n2. Creating demo collection...")
demo_collection = %{
  "id" => "demo-collection-#{:rand.uniform(10000)}",
  "title" => "Demo Collection",
  "description" => "Collection under demo catalog",
  "license" => "CC-BY-4.0",
  "catalog_id" => catalog_id,
  "extent" => %{
    "spatial" => %{"bbox" => [[-180, -90, 180, 90]]},
    "temporal" => %{"interval" => [["2024-01-01T00:00:00Z", nil]]}
  }
}

{:ok, collection_response} = Req.post(manage_req, url: "/collections", json: demo_collection)
collection_id = collection_response.body["data"]["id"]
IO.puts("   Created collection: #{collection_id}")

# 3. Verify resources exist
# Catalogs via manage (requires auth); collections via STAC API (public)
IO.puts("\n3. Verifying resources...")
{:ok, verify_response} = Req.get(manage_req, url: "/catalogs/#{catalog_id}")
IO.puts("   Catalog exists: #{verify_response.body["id"]}")

{:ok, collection_verify} = Req.get(req, url: "/collections/#{collection_id}")
IO.puts("   Collection exists: #{collection_verify.body["id"]}")

# 4. Update the catalog
IO.puts("\n4. Updating catalog title...")
{:ok, update_response} = Req.patch(manage_req,
  url: "/catalogs/#{catalog_id}",
  json: %{"id" => catalog_id, "title" => "Updated Demo Catalog"}
)
IO.puts("   New title: #{update_response.body["data"]["title"]}")

# 5. Clean up - cascade delete everything
IO.puts("\n5. Cleaning up (cascade delete)...")
{:ok, delete_response} = Req.delete(manage_req, url: "/catalogs/#{catalog_id}")
IO.puts("   Deleted successfully!")
IO.puts("   Cascade summary:")
IO.inspect(delete_response.body["cascade_deleted"], pretty: true)

# 6. Verify deletion
IO.puts("\n6. Verifying deletion...")
case Req.get(manage_req, url: "/catalogs/#{catalog_id}") do
  {:ok, %{status: 404}} -> IO.puts("   ✓ Catalog successfully deleted")
  {:ok, _} -> IO.puts("   ✗ Catalog still exists!")
end

Error Handling Examples

# Try to create a catalog without authentication (should return 401)
# The manage API always requires a valid RW key
no_auth_req = Req.new(base_url: base_url <> manage_version)

case Req.post(no_auth_req, url: "/catalogs", json: %{"id" => "test"}) do
  {:ok, %{status: 401} = response} ->
    IO.puts("Expected 401 Unauthorized:")
    IO.inspect(response.body)
  other ->
    IO.puts("Unexpected response:")
    IO.inspect(other)
end
# Try to get a non-existent catalog
case Req.get(manage_req, url: "/catalogs/definitely-does-not-exist") do
  {:ok, %{status: 404} = response} ->
    IO.puts("Expected 404 Not Found:")
    IO.inspect(response.body)
  other ->
    IO.puts("Unexpected response:")
    IO.inspect(other)
end
# Try to create a catalog with missing required fields
invalid_catalog = %{
  "title" => "No ID Catalog",
  "description" => "This is missing the required 'id' field"
}

case Req.post(manage_req, url: "/catalogs", json: invalid_catalog) do
  {:ok, %{status: 400} = response} ->
    IO.puts("Expected 400 Bad Request:")
    IO.inspect(response.body)
  other ->
    IO.puts("Unexpected response:")
    IO.inspect(other)
end

Helper Functions

Reusable helper functions. Pass manage_req (authenticated, manage base URL) for catalog operations, and req (public STAC API base URL) for browsing collections/items.

defmodule STACHelpers do
  @doc "List all catalogs. Pass manage_req (auth required)."
  def list_catalogs(manage_req) do
    case Req.get(manage_req, url: "/catalogs") do
      {:ok, %{status: 200, body: body}} -> {:ok, body["catalogs"]}
      {:ok, response} -> {:error, response}
      error -> error
    end
  end

  @doc "Get a specific catalog. Pass manage_req (auth required)."
  def get_catalog(manage_req, id) do
    case Req.get(manage_req, url: "/catalogs/#{id}") do
      {:ok, %{status: 200, body: body}} -> {:ok, body}
      {:ok, %{status: 404}} -> {:error, :not_found}
      {:ok, response} -> {:error, response}
      error -> error
    end
  end

  @doc "Create a catalog. Pass manage_req (RW key required)."
  def create_catalog(manage_req, catalog_data) do
    case Req.post(manage_req, url: "/catalogs", json: catalog_data) do
      {:ok, %{status: 201, body: body}} -> {:ok, body["data"]}
      {:ok, %{status: 409}} -> {:error, :already_exists}
      {:ok, response} -> {:error, response}
      error -> error
    end
  end

  @doc "Full replacement of a catalog. Pass manage_req (RW key required)."
  def update_catalog(manage_req, id, catalog_data) do
    case Req.put(manage_req, url: "/catalogs/#{id}", json: catalog_data) do
      {:ok, %{status: 200, body: body}} -> {:ok, body["data"]}
      {:ok, %{status: 404}} -> {:error, :not_found}
      {:ok, response} -> {:error, response}
      error -> error
    end
  end

  @doc "Delete a catalog (cascade). Pass manage_req (RW key required)."
  def delete_catalog(manage_req, id) do
    case Req.delete(manage_req, url: "/catalogs/#{id}") do
      {:ok, %{status: 200, body: body}} -> {:ok, body["cascade_deleted"]}
      {:ok, %{status: 404}} -> {:error, :not_found}
      {:ok, response} -> {:error, response}
      error -> error
    end
  end
end
# Use the helper functions
{:ok, catalogs} = STACHelpers.list_catalogs(manage_req)
IO.puts("Found #{length(catalogs)} catalogs")

# Create a catalog using helper
new_catalog = %{
  "id" => "helper-test-#{:rand.uniform(10000)}",
  "title" => "Created via Helper",
  "description" => "Test catalog"
}

case STACHelpers.create_catalog(manage_req, new_catalog) do
  {:ok, catalog} -> IO.puts("Created: #{catalog["id"]}")
  {:error, reason} -> IO.puts("Error: #{inspect(reason)}")
end