GeoServer Configuration Elixir Client
An Elixir library for interacting with GeoServer's REST API to manage workspaces, datastores, feature types, coverage stores, coverages, styles, and layer groups.
Prerequisites
- Elixir 1.17+
- A running GeoServer instance with the REST API enabled
- Valid GeoServer credentials
Installation
Add to your mix.exs:
def deps do
[
{:geoserver_config, "~> 0.4"}
]
endConnection
Every API function takes a GeoserverConfig.Connection as its first argument.
Build one at application startup and pass it wherever needed.
From environment variables (read at runtime, never at compile time):
export GEOSERVER_BASE_URL="http://localhost:8080/geoserver/rest"
export GEOSERVER_USERNAME="admin"
export GEOSERVER_PASSWORD="geoserver"conn = GeoserverConfig.Connection.from_env()From explicit values:
conn = GeoserverConfig.Connection.new(
"http://localhost:8080/geoserver/rest",
"admin",
"geoserver"
)From your application's config (config/runtime.exs):
# config/runtime.exs
config :my_app, :geoserver,
base_url: System.get_env("GEOSERVER_BASE_URL"),
username: System.get_env("GEOSERVER_USERNAME"),
password: System.get_env("GEOSERVER_PASSWORD")conn = GeoserverConfig.Connection.from_application_env(:my_app)
# custom key: GeoserverConfig.Connection.from_application_env(:my_app, :geo_api)Custom env prefix (useful for multiple GeoServer instances):
# reads STAGING_GEOSERVER_BASE_URL, STAGING_GEOSERVER_USERNAME, ...
conn = GeoserverConfig.Connection.from_env(prefix: "STAGING_GEOSERVER")Workspace Operations
{:ok, workspaces} = GeoserverConfig.Workspaces.fetch_workspaces(conn)
{:ok, workspace} = GeoserverConfig.Workspaces.get_workspace(conn, "ws_name")
{:ok, "new_ws"} = GeoserverConfig.Workspaces.create_workspace(conn, "new_ws")
{:ok, "new_name"} = GeoserverConfig.Workspaces.update_workspace(conn, "old_name", "new_name")
{:ok, "old_ws"} = GeoserverConfig.Workspaces.delete_workspace(conn, "old_ws")Note: GeoServer may reject workspace renames depending on its version and configuration.
Datastore Operations
{:ok, stores} = GeoserverConfig.Datastores.list_datastores(conn, "workspace_name")
{:ok, store} = GeoserverConfig.Datastores.get_datastore(conn, "workspace_name", "my_store")
# With quiet-on-not-found (avoids server-side logging on 404)
{:ok, store} = GeoserverConfig.Datastores.get_datastore(conn, "workspace_name", "my_store", quiet_on_not_found: true)Shapefile (with enhanced options):
# Basic shapefile directory
{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
conn,
"workspace_name",
"my_store",
"shapefile",
%{url: "file:///path/to/shapefile_directory"}
)
# Shapefile with charset support
{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
conn,
"workspace_name",
"my_store",
"shapefile",
%{url: "file:///path/to/shapes", charset: "ISO-8859-1"}
)PostGIS (with comprehensive connection parameters):
# Basic PostGIS connection
{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
conn,
"workspace_name",
"my_store",
"postgis",
%{
host: "localhost",
port: 5432,
database: "db_name",
user: "db_user",
passwd: "db_password"
}
)
# Enhanced PostGIS with connection pooling and performance settings
{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
conn,
"workspace_name",
"my_store",
"postgis",
%{
host: "localhost",
port: 5432,
database: "db_name",
user: "db_user",
passwd: "db_password",
schema: "custom_schema",
"max connections": "20",
"min connections": "5",
"Connection timeout": "30",
"Loose bbox": "true",
"Estimated extends": "true",
"Expose primary keys": "true"
}
)GeoPackage (with table support):
# Basic GeoPackage
{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
conn,
"workspace_name",
"my_store",
"geopkg",
%{database: "file:///path/to/file.gpkg"}
)
# GeoPackage with specific table
{:ok, "my_store"} = GeoserverConfig.Datastores.create_datastore(
conn,
"workspace_name",
"my_store",
"geopkg",
%{database: "file:///path/to/file.gpkg", table: "my_layer"}
)Update:
{:ok, "my_store"} = GeoserverConfig.Datastores.update_datastore(
conn,
"workspace_name",
"my_store",
"shapefile",
%{description: "New description", url: "file:///new/path"}
)Delete (recurse: true also removes dependent feature types):
{:ok, "my_store"} = GeoserverConfig.Datastores.delete_datastore(conn, "workspace_name", "my_store", true)File upload (create or update a datastore by uploading spatial data):
# Upload a shapefile from a local file path
{:ok, "my_store"} = GeoserverConfig.Datastores.upload_datastore(
conn,
"workspace_name",
"my_store",
:file,
"shp",
File.read!("/path/to/shapefile.zip"),
content_type: "application/zip"
)
# Upload from a remote URL
{:ok, "my_store"} = GeoserverConfig.Datastores.upload_datastore(
conn,
"workspace_name",
"my_store",
:url,
"shp",
"https://example.com/data.zip"
)
# Upload using an existing server-side file
{:ok, "my_store"} = GeoserverConfig.Datastores.upload_datastore(
conn,
"workspace_name",
"my_store",
:external,
"shp",
"file:///data/shapes.zip",
configure: "all", update: "overwrite", charset: "ISO-8859-1"
)Reset (drop cached structures, force reconnect):
{:ok, "my_store"} = GeoserverConfig.Datastores.reset_datastore(conn, "workspace_name", "my_store")Feature Types (Vector Layers)
Feature types represent vector layers published from datastores. These operations allow you to manage vector data layers for WMS/WFS services.
Datastore-scoped operations
# List all configured feature types in a datastore
{:ok, feature_types} = GeoserverConfig.list_featuretypes(conn, "workspace_name", "datastore_name")
# Get a single feature type
{:ok, feature_type} = GeoserverConfig.get_featuretype(conn, "workspace_name", "datastore_name", "my_layer")
# With quiet-on-not-found
{:ok, feature_type} = GeoserverConfig.get_featuretype(conn, "workspace_name", "datastore_name", "my_layer", quiet_on_not_found: true)
# List available (unpublished) feature types
{:ok, available_types} = GeoserverConfig.list_featuretypes(conn, "workspace_name", "datastore_name", :available)
# List all feature types (configured + available)
{:ok, all_types} = GeoserverConfig.list_featuretypes(conn, "workspace_name", "datastore_name", :all)Create vector layers with comprehensive metadata:
{:ok, "my_layer"} = GeoserverConfig.create_featuretype(
conn,
"workspace_name",
"datastore_name",
"my_layer",
%{
title: "My Vector Layer",
description: "Detailed description of the layer",
abstract: "Abstract text for metadata",
srs: "EPSG:4326",
native_crs: "EPSG:3857",
native_bbox: %{minx: -180.0, maxx: 180.0, miny: -90.0, maxy: 90.0},
latlon_bbox: %{minx: -180.0, maxx: 180.0, miny: -90.0, maxy: 90.0},
enabled: true,
keywords: ["vector", "roads", "transportation"],
metadata: %{"cacheAgeMax" => 3600, "cachingEnabled" => true},
projection_policy: "REPROJECT_TO_DECLARED",
max_features: 1000,
num_decimals: 6,
cql_filter: "INCLUDE",
overriding_service_srs: true
}
)Update existing vector layers:
{:ok, "my_layer"} = GeoserverConfig.update_featuretype(
conn,
"workspace_name",
"datastore_name",
"my_layer",
%{
title: "Updated Title",
description: "Updated description",
srs: "EPSG:3857"
}
)
# Update with bounding box recalculation
{:ok, "my_layer"} = GeoserverConfig.update_featuretype(
conn,
"workspace_name",
"datastore_name",
"my_layer",
%{title: "Updated Title"},
"nativebbox,latlonbbox" # Recalculate both bounding boxes
)Delete vector layers:
# Delete layer (recurse: true removes dependent resources)
{:ok, "my_layer"} = GeoserverConfig.delete_featuretype(
conn,
"workspace_name",
"datastore_name",
"my_layer",
true # recurse
)Reset (drop cached feature type structures):
{:ok, "my_layer"} = GeoserverConfig.reset_featuretype(conn, "workspace_name", "datastore_name", "my_layer")Workspace-level operations
These operate across all datastores in a workspace, matching GeoServer's alternate REST paths.
# List all feature types across all datastores
{:ok, types} = GeoserverConfig.list_workspace_featuretypes(conn, "workspace_name")
{:ok, available} = GeoserverConfig.list_workspace_featuretypes(conn, "workspace_name", :available)
# Get a single feature type
{:ok, ft} = GeoserverConfig.get_workspace_featuretype(conn, "workspace_name", "my_layer")
# Create (must reference a store)
{:ok, "my_layer"} = GeoserverConfig.create_workspace_featuretype(
conn,
"workspace_name",
"my_layer",
%{title: "My Layer", srs: "EPSG:4326", store: %{name: "my_store"}}
)
# Update (with bounding box recalculation)
{:ok, "my_layer"} = GeoserverConfig.update_workspace_featuretype(
conn,
"workspace_name",
"my_layer",
%{title: "New Title"},
"nativebbox,latlonbbox"
)
# Delete
{:ok, "my_layer"} = GeoserverConfig.delete_workspace_featuretype(conn, "workspace_name", "my_layer", true)Coverage Store Operations
{:ok, stores} = GeoserverConfig.Coveragestores.list_coveragestores(conn, "workspace_name")
{:ok, store} = GeoserverConfig.Coveragestores.get_coveragestore(conn, "workspace_name", "dem_store")Local GeoTIFF:
{:ok, "dem_store"} = GeoserverConfig.Coveragestores.create_coveragestore(
conn,
"workspace_name",
"dem_store",
"file:///path/to/geotiff.tif",
"Optional description"
)Cloud Optimized GeoTIFF (COG) via S3 or HTTP:
{:ok, "dem_store"} = GeoserverConfig.Coveragestores.create_coveragestore(
conn,
"workspace_name",
"dem_store",
"cog://https://path.to/your/file_cog.tif",
"COG from HTTP",
%{
metadata: %{
"entry" => %{
"@key" => "CogSettings.Key",
"cogSettings" => %{
"useCachingStream" => false,
"rangeReaderSettings" => "HTTP"
}
}
},
disableOnConnFailure: false
}
)Update:
{:ok, "dem_store"} = GeoserverConfig.Coveragestores.update_coveragestore(
conn,
"workspace_name",
"dem_store",
%{
type: "GeoTIFF",
enabled: true,
url: "file:///new/path/to/file.tif",
description: "Updated description"
}
)Delete (uses purge=true to remove related resources):
{:ok, "dem_store"} = GeoserverConfig.Coveragestores.delete_coveragestore(conn, "workspace_name", "dem_store")Coverage Layer Operations
{:ok, coverages} = GeoserverConfig.Coverages.list_coverages(conn, "workspace_name", "dem_store")
{:ok, coverage} = GeoserverConfig.Coverages.get_coverage(conn, "workspace_name", "dem_store", "dem_layer")Create:
{:ok, "dem_layer"} = GeoserverConfig.Coverages.create_coverage(
conn,
"workspace_name",
"dem_store",
"dem_layer",
%{
title: "DEM Layer",
description: "Digital Elevation Model",
abstract: "Raster coverage layer",
srs: "EPSG:3301",
native_crs: "EPSG:3301",
native_bbox: %{minx: 369000.0, maxx: 740000.0, miny: 6377000.0, maxy: 6635000.0},
latlon_bbox: %{minx: 21.664, maxx: 28.275, miny: 57.471, maxy: 59.831},
grid: %{
dimension: [3710, 2580],
transform: [10.0, 0.0, 369000.0, 0.0, -10.0, 6635000.0]
},
metadata: %{
"cacheAgeMax" => 3600,
"cachingEnabled" => true
}
},
"file:///path/to/geotiff.tif"
)Update:
{:ok, "dem_layer"} = GeoserverConfig.Coverages.update_coverage(
conn,
"workspace_name",
"dem_store",
"dem_layer",
%{title: "Updated DEM", description: "Updated description"}
)Delete (recurse: true also removes dependent resources):
{:ok, "dem_layer"} = GeoserverConfig.Coverages.delete_coverage(conn, "workspace_name", "dem_store", "dem_layer", true)Style Operations
{:ok, styles} = GeoserverConfig.Styles.list_styles(conn)
{:ok, styles} = GeoserverConfig.Styles.list_styles_workspace_specific(conn, "workspace_name")Get style content (pass nil as workspace for global styles):
{:ok, sld_xml} = GeoserverConfig.Styles.get_style(conn, "workspace_name", "style_name")
{:ok, sld_xml} = GeoserverConfig.Styles.get_style(conn, nil, "global_style")
# Save to disk (no connection needed)
{:ok, %{file_path: path, size: bytes}} = GeoserverConfig.Styles.write_sld_file("/tmp/style.sld", sld_xml)Create SLD or CSS styles with auto-detection:
# SLD style (explicit format)
{:ok, "sld_style"} = GeoserverConfig.Styles.create_style(conn, %{
name: "sld_style",
content: "<StyledLayerDescriptor>...</StyledLayerDescriptor>",
format: :sld,
workspace: "workspace_name"
})
# CSS style (explicit format)
{:ok, "css_style"} = GeoserverConfig.Styles.create_style(conn, %{
name: "css_style",
content: "* { stroke: red; fill: blue; }",
format: :css
})
# Auto-detect format from filename
{:ok, "auto_style"} = GeoserverConfig.Styles.create_style(conn, %{
name: "auto_style",
content: "* { stroke: red; }",
filename: "style.css" # Auto-detects CSS format
})
# Global style (omit workspace)
{:ok, "global_style"} = GeoserverConfig.Styles.create_style(conn, %{
name: "global_style",
content: "<StyledLayerDescriptor>...</StyledLayerDescriptor>"
})Update styles (supports both SLD and CSS):
{:ok, "my_style"} = GeoserverConfig.Styles.update_style(conn, %{
name: "my_style",
content: "<StyledLayerDescriptor>...</StyledLayerDescriptor>",
format: :sld,
workspace: "workspace_name"
})
# Update CSS style
{:ok, "css_style"} = GeoserverConfig.Styles.update_style(conn, %{
name: "css_style",
content: "* { stroke: blue; }",
format: :css
})Copy styles between workspaces:
# Copy from global to workspace
{:ok, "ws_style"} = GeoserverConfig.Styles.copy_style(
conn,
"global_style",
nil, # source workspace (nil = global)
"ws_style", # target name
"target_workspace" # target workspace
)
# Copy between workspaces
{:ok, "copied_style"} = GeoserverConfig.Styles.copy_style(
conn,
"source_style",
"source_workspace",
"copied_style",
"target_workspace"
)Move styles between workspaces:
# Move from workspace to global
{:ok, "moved_style"} = GeoserverConfig.Styles.move_style(
conn,
"style_name",
"source_workspace",
nil # target workspace (nil = global)
)
# Move between workspaces with purge
{:ok, "moved_style"} = GeoserverConfig.Styles.move_style(
conn,
"style_name",
"source_workspace",
"target_workspace",
purge: true # Purge original files
)Delete styles:
# Delete workspace style with purge
{:ok, "my_style"} = GeoserverConfig.Styles.delete_style(
conn,
"my_style",
"workspace_name",
purge: true,
recurse: true
)
# Delete global style
{:ok, "my_style"} = GeoserverConfig.Styles.delete_style(conn, "my_style")Assign Style to Layer
Verifies the style exists before assigning it:
# Style from the same or global scope
{:ok, msg} = GeoserverConfig.StyleAssignToLayer.assign_style_to_layer(
conn,
"workspace_name",
"layer_name",
"style_name"
)
# Style from a specific workspace
{:ok, msg} = GeoserverConfig.StyleAssignToLayer.assign_style_to_layer(
conn,
"workspace_name",
"layer_name",
"style_name",
"style_workspace"
)Unassign (remove) the default style from a layer:
# Dedicated function
{:ok, msg} = GeoserverConfig.unassign_style_from_layer(
conn,
"workspace_name",
"layer_name"
)
# Or pass nil/"" as the style name to assign_style_to_layer
{:ok, msg} = GeoserverConfig.assign_style_to_layer(
conn,
"workspace_name",
"layer_name",
nil
)Layer Group Operations
{:ok, groups} = GeoserverConfig.LayerGroups.list_layer_groups(conn)
{:ok, group} = GeoserverConfig.LayerGroups.get_layer_group(conn, "my-group")
# Create from XML or a map
{:ok, _} = GeoserverConfig.LayerGroups.create_layer_group(conn, xml_string)
{:ok, _} = GeoserverConfig.LayerGroups.create_layer_group(conn, %{"layerGroup" => %{"name" => "my-group"}})
# Update
{:ok, _} = GeoserverConfig.LayerGroups.update_layer_group(conn, "my-group", updated_xml)
# Add / remove layers
{:ok, _} = GeoserverConfig.LayerGroups.add_layer_to_group(conn, "my-group", "ws:layer1", "ws:style1")
{:ok, _} = GeoserverConfig.LayerGroups.remove_layer_from_group(conn, "my-group", "ws:layer1")
{:ok, "my-group"} = GeoserverConfig.LayerGroups.delete_layer_group(conn, "my-group")Error Handling
All functions return tagged tuples. Pattern match to handle each case:
case GeoserverConfig.Workspaces.fetch_workspaces(conn) do
{:ok, workspaces} ->
IO.inspect(workspaces)
{:error, {:http_error, status, body}} ->
IO.puts("GeoServer returned #{status}: #{inspect(body)}")
{:error, {:not_found, name}} ->
IO.puts("#{name} does not exist")
{:error, {:request_failed, reason}} ->
IO.puts("Transport error: #{inspect(reason)}")
endNotes
-
Use
file://prefix for local file paths passed to GeoServer (e.g.file:///data/dem.tif) -
Use
cog://prefix for Cloud Optimized GeoTIFFs served over HTTP or S3 - Coverage layer creation requires bounding box and CRS information matching the source raster
- Feature types (vector layers) support comprehensive metadata including bounding boxes and keywords
-
Styles can be scoped globally or per workspace; pass
nilas workspace for global styles - Style format (SLD vs CSS) is auto-detected from content or can be specified explicitly
recurse: true/purge: trueoptions cascade deletes to dependent resources- Style copy/move operations preserve all style content and metadata
- PostGIS datastores support comprehensive connection pooling and performance parameters
add_layer_to_groupandremove_layer_from_groupautomatically maintain the layer:style count parity required by GeoServer-
Use
list_featuretypes(conn, ws, store, :available)to list unpublished feature types for PostGIS and GeoPackage datastores assign_style_to_layeracceptsnilor""as the style name to remove the default style; useunassign_style_from_layerfor clarity-
Delete operations return
{:skipped, name}on 404 — treat this as idempotent success
License
MIT License