A Go client for the GeoServer REST API.
Provision workspaces, register PostGIS / GeoTIFF / Shapefile data sources, publish layers, manage styles, configure security, drive GeoWebCache, and run the Importer extension — all from idiomatic Go, with mandatory
context.Context, typed errors, and zero runtime third-party dependencies.
- What is GeoServer?
- What this client does
- Install
- Quick start
- Worked example: publish a PostGIS layer
- Errors
- Authentication & advanced configuration
- Examples & further reading
- Version compatibility
- Contributing
GeoServer is an open-source Java server that publishes geographic data via the OGC web standards: WMS (rendered map images), WFS (vector features), WCS (raster coverages), and WMTS (pre-rendered tiles). It's deployed by mapping companies, government agencies, and GIS teams to put PostGIS tables, GeoTIFFs, Shapefiles, and remote services behind a single web-services endpoint.
This package is for Go programs that need to provision or operate a GeoServer — typically pipelines that publish new data, infrastructure code that manages workspaces and security, or back-office tools that drive the Importer or seed GeoWebCache.
The client surface is broken into typed sub-clients on *geoserver.Client. Each bullet below names what you'd accomplish; the trailing fields are the entry points.
- Catalog & publishing — workspaces, datastores, feature types, coverage stores, coverages, layers, layer groups, styles, namespaces; file-upload publishing for Shapefile / GeoPackage / GeoTIFF / mosaic granules; layer–style associations.
Entry points:
c.Workspaces,c.Datastores,c.FeatureTypes,c.CoverageStores,c.Coverages,c.Layers,c.LayerGroups,c.Styles,c.Namespaces. - OGC services — per-service WMS / WFS / WCS / WMTS configuration (global + per-workspace overrides),
GetCapabilities,DescribeFeatureType,DescribeCoverage, cascaded WMS/WMTS stores + layers, WFS XSLT transforms. Entry points:c.Services.WMS()/WFS()/WCS()/WMTS(),c.WMS,c.WFS,c.WCS,c.WMSStores,c.WMSLayers,c.WMTSStores,c.WMTSLayers,c.WFSTransforms. - Tile caching — GeoWebCache layer config, seed / reseed / truncate, disk quota, gridsets, mass-truncate, global GWC settings.
Entry point:
c.GWC.Layers()/Seed()/DiskQuota()/Global()/Gridsets()/MassTruncate(). - Security — users, groups, roles, full ACL surface (layers, services, REST, catalog), auth providers / filters / chains, URL checks (SSRF allow-list), master & self password rotation.
Entry points:
c.Security,c.ACL.Layers()/Services()/REST()/Catalog(),c.URLChecks. - Operations — system reload, cache reset, runtime logging, monitoring (
gs-monitor), manifests, system status, fonts, global settings. Entry points:c.System,c.Settings,c.About,c.Logging,c.Monitor,c.Fonts. - Data plane & extras — Resource API (read/write any file under the data dir), FTL templates, Importer extension (batch ingest).
Entry points:
c.Resources,c.Templates,c.Imports.
The full method-level reference lives at pkg.go.dev.
go get github.com/hishamkaram/geoserver/v2@latestimport geoserver "github.com/hishamkaram/geoserver/v2"Requirements: Go 1.25+. Tested against GeoServer 2.27.4 LTS and 2.28.0 stable on every PR.
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"time"
geoserver "github.com/hishamkaram/geoserver/v2"
"github.com/hishamkaram/geoserver/v2/rest/workspaces"
)
func main() {
c, err := geoserver.New("http://localhost:8080/geoserver/",
geoserver.WithBasicAuth("admin", "geoserver"),
geoserver.WithTimeout(10*time.Second),
geoserver.WithLogger(slog.New(slog.NewTextHandler(os.Stderr, nil))),
)
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
list, err := c.Workspaces.List(ctx, workspaces.ListOptions{})
if err != nil {
panic(err)
}
for _, ws := range list {
fmt.Println(ws.Name)
}
_, err = c.Workspaces.Get(ctx, "doesnotexist")
if errors.Is(err, geoserver.ErrNotFound) {
fmt.Println("workspace doesn't exist")
}
}*Client is immutable after construction and safe for concurrent use across goroutines. Sub-clients hold no shared mutable state of their own.
The canonical end-to-end flow — create a workspace, register a PostGIS datastore, publish a feature type, fetch the auto-created layer back. Compile-checked from examples/publish-postgis/main.go; run it with go run ./examples/publish-postgis against a make compose-up stack.
import (
geoserver "github.com/hishamkaram/geoserver/v2"
"github.com/hishamkaram/geoserver/v2/rest/datastores"
"github.com/hishamkaram/geoserver/v2/rest/featuretypes"
"github.com/hishamkaram/geoserver/v2/rest/workspaces"
)
// 1. Workspace.
_ = c.Workspaces.Create(ctx, &workspaces.Workspace{Name: "demo"})
// 2. PostGIS datastore (workspace-scoped via InWorkspace).
ds := c.Datastores.InWorkspace("demo")
_ = ds.Create(ctx, datastores.PostGIS{
Name: "lbldyt_pg",
Host: "postgis",
Port: 5432,
Database: "geoserver",
User: "postgres",
Password: "postgres",
})
// 3. 2-level scope: feature types live under (workspace, datastore).
ft := c.FeatureTypes.InWorkspace("demo").InDatastore("lbldyt_pg")
// 4. Discover available tables not yet configured.
available, _ := ft.Discover(ctx, featuretypes.DiscoverOptions{
Kind: featuretypes.DiscoverAvailableWithGeometry,
})
fmt.Println("available:", available)
// 5. Publish one as a feature type — NativeName must match the DB table.
_ = ft.Create(ctx, &featuretypes.FeatureType{
Name: "lbldyt", NativeName: "lbldyt",
SRS: "EPSG:4326", Enabled: true,
})
// 6. Fetch the auto-created layer.
layer, _ := c.Layers.InWorkspace("demo").Get(ctx, "lbldyt")
fmt.Printf("layer: %s (queryable=%t)\n", layer.Name, layer.Queryable)The same InWorkspace(...).In<Parent>(...) pattern applies to coverages under a coverage store (see rest/coverages/).
Every non-2xx GeoServer response surfaces as *geoserver.APIError and wraps one of twelve package sentinels — ErrBadRequest, ErrUnauthorized, ErrForbidden, ErrNotFound, ErrMethodNotAllowed, ErrConflict, ErrUnsupportedMediaType, ErrRateLimited, ErrServerError, ErrBadGateway, ErrServiceUnavailable, ErrGatewayTimeout. Match with errors.Is; inspect status code, op, method, URL, and the (8 KiB-capped) response body via errors.As.
var apiErr *geoserver.APIError
switch {
case errors.Is(err, geoserver.ErrNotFound):
// 404 — workspace, layer, etc. not present.
case errors.Is(err, geoserver.ErrConflict):
// 409 — resource already exists.
case errors.As(err, &apiErr):
log.Printf("op=%s status=%d body=%s", apiErr.Op, apiErr.StatusCode, apiErr.Body)
}Never compare error strings — only errors.Is / errors.As are supported. See examples/error-handling/main.go for a runnable demo of all twelve sentinels.
All transport-level concerns are configured at construction via functional options. The constructor returns immediately; nothing in *Client is mutable after New returns.
c, _ := geoserver.New(serverURL,
// Auth — pick one (or compose your own RoundTripper):
geoserver.WithBasicAuth("admin", "geoserver"),
geoserver.WithBearerToken(os.Getenv("GS_TOKEN")),
// Transport / observability:
geoserver.WithHTTPClient(myClient), // bring your own *http.Client
geoserver.WithTransport(otelhttp.NewTransport(...)), // custom http.RoundTripper
geoserver.WithTimeout(30 * time.Second),
geoserver.WithUserAgent("my-pipeline/1.4"),
geoserver.WithHeader("X-Trace-Id", traceID),
// Logging — defaults to slog.DiscardHandler.
geoserver.WithLogger(slog.New(slog.NewTextHandler(os.Stderr, nil))),
)WithTransport is the natural integration point for OpenTelemetry, Vault-rotated credentials, and retry libraries — anything that satisfies http.RoundTripper composes without re-authenticating per call.
Pagination. Every List endpoint has a sibling Iter returning iter.Seq2[T, error] for range-over-func streaming:
for ws, err := range c.Workspaces.Iter(ctx, workspaces.ListOptions{}) {
if err != nil {
return err
}
fmt.Println(ws.Name)
}Self-contained main packages under examples/:
workspaces/— flat sub-client CRUD;errors.Ismatching;Iterrange-over-func.publish-postgis/— end-to-end workspace → datastore → feature type → layer flow.style-upload/— two-step style publish (Createmetadata +UploadSLDbody).error-handling/— every sentinel +*APIErrorinspection viaerrors.As.
Run any with go run ./examples/<name> against a make compose-up stack, or compile-check all with make examples.
Reference docs:
- pkg.go.dev godoc — full method-level API reference.
docs/architecture.md— sub-client pattern, transport layer, error model.docs/geoserver-rest-quirks.md— public catalog of GeoServer 2.x REST API edge cases this client works around.docs/version-compat.md— Go and GeoServer version matrix.ROADMAP.mdandCHANGELOG.md.
- Go: 1.25+.
- GeoServer: 2.27 LTS and 2.28 stable. Both legs run on every PR via the integration matrix in
.github/workflows/integration.yml. - Module path:
github.com/hishamkaram/geoserver/v2. The/v2suffix is required by Go's semantic import versioning rule for v2+ modules. - API stability: v2's public API is locked — no breaking changes will land in v2.x.
- v1: end-of-feature on the
release/v1branch (security patches only). Existing v1 users: seedocs/migration-v1-to-v2.mdfor the per-resource upgrade mapping.
To add a new sub-client, pick the reference shape that matches your resource and follow the existing layout:
- Flat CRUD:
rest/workspaces/,rest/namespaces/. - Workspace-scoped:
rest/datastores/,rest/coveragestores/,rest/layers/. - 2-level hierarchy:
rest/featuretypes/,rest/coverages/. - Generic-typed dispatch:
rest/services/(per-service WMS/WFS/WCS/WMTS). - Out-of-
/rest/URL prefix:rest/gwc/(paths under/gwc/rest/). - XML wire format:
ows/wms/,ows/wfs/,ows/wcs/.
Each sub-client is structured the same way:
types.go— wire-format request/response structs, public option types, custom(Un)MarshalJSONfor any wire quirks.<resource>.go—type Client struct{ core Core },func New(core Core), methods. Each subpackage'sCoreinterface declares only the transport methods it actually uses (Do/DoXML/DoRaw/DoStream).<resource>_test.go(httptest unit tests) and<resource>_integration_test.gowith the//go:build integrationtag.- Wire into
*Clientingeoserver.go.
Run integration tests locally before pushing: make compose-up && go test -tags=integration ./rest/<resource>/. CI runs the full matrix on real GeoServer 2.27.4 LTS + 2.28.0 stable, but local-first catches wire-format quirks faster.
See CONTRIBUTING.md for the general PR workflow and docs/geoserver-rest-quirks.md for the catalog of known REST API edge cases.