Skip to main content

FeatureService

Integrates ArcGIS Feature Services with MapLibre GL JS and Mapbox GL JS. Features are requested per viewport tile and fed into a MapLibre GeoJSON source — using the layer's binary PBF query format when the service advertises it, and falling back to GeoJSON when it doesn't. Also supports server-side filtering, feature editing, and attachments.

Live Demo

Interactive example showing FeatureService with various ArcGIS Feature Services

Quick Start

import { FeatureService } from 'esri-gl';

const featureService = new FeatureService('features-source', map, {
url: 'https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Landscape_Trees/FeatureServer/0'
});

map.addLayer({
id: 'features-layer',
type: 'circle',
source: 'features-source',
paint: {
'circle-radius': 5,
'circle-color': '#007cbf',
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff'
}
});

Constructor

ArgumentTypeDescription
idstringAn id to assign to the MapLibre GL source
mapMapA MapLibre GL or Mapbox GL map instance
esriServiceOptionsobjectOptions for the Feature Service (see below)
geoJsonSourceOptionsobjectOptional MapLibre GL GeoJSON source options

Options

These options shape the tile requests the service makes as the map moves.

OptionTypeDefaultDescription
urlstringRequired. URL of the FeatureService layer (ends in a layer index), or an ArcGIS portal item id
portalstringPortal sharing REST URL used to resolve an item id url (defaults to ArcGIS Online)
layerIdnumber0Sublayer appended when an item id resolves to a service root
wherestring'1=1'SQL WHERE clause to filter features
outFieldsArray<string> | string'*'Fields to include in the response (the layer's unique id field is always added)
fromDate | number | nullnullStart of the time extent (applied together with to)
toDate | number | nullnullEnd of the time extent
minZoomnumber2 (7 when useStaticZoomLevel)Zoom below which no features are requested
useStaticZoomLevelbooleanfalseAlways request tiles at minZoom instead of tracking the map's zoom
simplifyFactornumber0.3Geometry simplification factor (0–1)
precisionnumber8Decimal precision of returned coordinates
useServiceBoundsbooleantrueSkip tiles outside the service extent
projectionEndpointstringderived from urlGeometryServer project endpoint used to reproject the service extent
setAttributionFromServicebooleantrueFetch copyright text from service metadata
tokenstringAuthentication token (sent as the token parameter)
apiKeystringArcGIS Location Platform API key (sent as the token parameter)
authenticationIAuthenticationManager | stringArcGIS REST JS auth manager (preferred for OAuth/user sign-in)
fetchOptionsobjectDeprecated — no longer forwarded to requests; use authentication instead.

The remaining query fields (geometry, geometryType, spatialRel, inSR, outSR, orderByFields, outStatistics, having, resultOffset, resultRecordCount, …) are defaults for queryFeatures() rather than for the map source.

Ignored options

useVectorTiles, useBoundingBox and maxRecordCount are accepted for backwards compatibility but are not applied. The query format is negotiated from the layer's supportedQueryFormats, viewport loading is always on (toggle it with disableRequests() / enableRequests()), and page size is governed by the server's own maxRecordCount.

Authentication runs on ArcGIS REST JS. See the Authentication guide for tokens, API keys, and auth managers.

Methods

MethodReturnsDescription
queryFeatures(options?)Promise<GeoJSON.FeatureCollection>Run a one-off query with custom parameters (does not change the map source). Always requests f=geojson, so use the Query task for outStatistics queries, which services only answer as f=json.
getFeaturesByLonLat(lngLat, radius?, returnGeometry?)Promise<GeoJSON.FeatureCollection>Features within radius metres (default 20) of a point
getFeaturesByObjectIds(objectIds, returnGeometry?)Promise<GeoJSON.FeatureCollection>Features for a list of object ids
getStyle()Promise<StyleData>A layer style matching the service geometry type
setWhere(where)voidReplace the WHERE clause and reload the visible tiles
clearWhere()voidReset the WHERE clause to '1=1' and reload
setOutFields(fields)voidReplace the output fields and reload
setDate(to, from?)voidSet the time extent and reload. Note the argument orderto first, unlike DynamicMapService.setDate(from, to)
setToken(token)voidUpdate the authentication token and reload
enableRequests()voidStart loading features on map moveend (on by default)
disableRequests()voidStop loading features on map movement
queryRelatedRecords(options)Promise<IQueryRelatedResponse>Query records related through a relationship class
decodeValues(queryResponse, fields?)Promise<IQueryFeaturesResponse>Replace coded-value-domain codes with their labels
remove()voidRemove service and clean up resources

Properties

PropertyTypeDescription
sourceReadyPromise<void>Resolves once the source is on the map and the layer metadata has loaded; rejects if the layer supports neither PBF nor GeoJSON
serviceMetadataobject | nullThe layer definition document, once loaded
supportsPbf / supportsGeojsonbooleanWhich query formats the layer advertises
defaultStyleStyleDataStyle for the layer's geometry type (available after metadata loads)
Deprecated aliases

updateSource(), updateData(), setBoundingBoxFilter(enabled), setLayers(), setGeometry() and clearGeometry() remain for backwards compatibility. Use setWhere() / enableRequests() / disableRequests() / queryFeatures() instead.

Editing Methods

Methods for creating, updating, and deleting features on editable Feature Services.

addFeatures(features, options?)

ParameterTypeDescription
featuresGeoJSON.Feature[]Features to add
options{ gdbVersion?: string }Optional geodatabase version

Returns: Promise<EditResult[]>

updateFeatures(features, options?)

ParameterTypeDescription
featuresGeoJSON.Feature[]Features to update (must include OBJECTID)
options{ gdbVersion?: string }Optional geodatabase version

Returns: Promise<EditResult[]>

deleteFeatures(params)

ParameterTypeDescription
params.objectIdsnumber[]Object IDs to delete
params.wherestringSQL WHERE clause to select features for deletion

Returns: Promise<EditResult[]>

applyEdits(edits, options?)

ParameterTypeDescription
edits.addsGeoJSON.Feature[]Features to add
edits.updatesGeoJSON.Feature[]Features to update
edits.deletesnumber[]Object IDs to delete
options{ gdbVersion?: string }Optional geodatabase version

Returns: Promise<ApplyEditsResult>

const service = new FeatureService('editable-source', map, {
url: 'https://services.arcgis.com/.../FeatureServer/0',
token: 'your-agol-token'
});

// Batch edits in a single request
const results = await service.applyEdits({
adds: [{
type: 'Feature',
geometry: { type: 'Point', coordinates: [-118.24, 34.05] },
properties: { name: 'Los Angeles' }
}],
updates: [{
type: 'Feature',
geometry: { type: 'Point', coordinates: [-118.24, 34.05] },
properties: { OBJECTID: 1, population: 4000000 }
}],
deletes: [10, 11, 12]
});

Attachment Methods

queryAttachments(objectId, options?)

ParameterTypeDescription
objectIdnumberObject ID of the feature

Returns: Promise<AttachmentInfo[]>

addAttachment(objectId, file, fileName?)

ParameterTypeDescription
objectIdnumberObject ID of the feature
fileBlob | FileThe file to attach
fileNamestringOptional file name

Returns: Promise<EditResult>

deleteAttachments(objectId, attachmentIds)

ParameterTypeDescription
objectIdnumberObject ID of the feature
attachmentIdsnumber[]IDs of attachments to delete

Returns: Promise<EditResult[]>

Events

EventPayloadDescription
authenticationrequired{ authenticate: (token: string) => void }Fired when a tile request fails with a 498/499 auth error. ArcGIS Online returns these as HTTP 200 with a JSON error body; the service detects and surfaces them automatically.
service.on('authenticationrequired', async ({ authenticate }) => {
const newToken = await refreshToken();
authenticate(newToken); // same as service.setToken(newToken)
});

Examples

Query Format Detection

The query format is chosen from the layer's supportedQueryFormats — no option required. PBF is used when the layer advertises it (smaller payloads, quantized geometry), GeoJSON otherwise:

const service = new FeatureService('smart-source', map, {
url: 'https://services.arcgis.com/.../FeatureServer/0'
});

await service.sourceReady;
console.log(service.supportsPbf, service.supportsGeojson);

Server-Side Filtering

const filtered = new FeatureService('filtered-source', map, {
url: 'https://services.arcgis.com/.../FeatureServer/0',
where: "SPECIES = 'Oak' AND HEIGHT > 20",
outFields: 'SPECIES,HEIGHT,DIAMETER'
});

Filtering after construction

service.setWhere("SPECIES = 'Oak'"); // re-requests the visible tiles
service.setOutFields(['SPECIES', 'HEIGHT']);
service.clearWhere();

Automatic Styling with getStyle()

Returns a layer style matching the service geometry type — circle for points, line for lines, fill for polygons — with source already set to the service's source id.

const layerStyle = await featureService.getStyle();
map.addLayer({ id: 'auto-styled-layer', ...layerStyle });

One-off queries

queryFeatures() runs an independent query against the layer; it does not change what the map source displays.

const fc = await featureService.queryFeatures({
where: 'HEIGHT > 20',
outFields: ['SPECIES', 'HEIGHT'],
resultRecordCount: 50
});

const nearby = await featureService.getFeaturesByLonLat({ lng: -118.24, lat: 34.05 }, 500);
const byId = await featureService.getFeaturesByObjectIds([1, 2, 3]);

Authentication

// Token auth (URL parameter)
const service = new FeatureService('source', map, {
url: 'https://services.arcgis.com/.../FeatureServer/0',
token: 'your-auth-token'
});

// API key auth (sent as the token parameter)
const service2 = new FeatureService('source', map, {
url: 'https://services.arcgis.com/.../FeatureServer/0',
apiKey: 'your-api-key'
});

// Update token dynamically
service.setToken('refreshed-token');