For the complete documentation index, see llms.txt. This page is also available as Markdown.
FundamentalsBeta

GraphQL basics

Learn GraphQL fundamentals for Navixy GraphQL API: how it differs from REST, queries, mutations, subscriptions, record IDs, and core type concepts.

This article introduces GraphQL for developers who are new to it. If you've worked with REST APIs before, you'll find GraphQL takes a different approach to requesting and modifying data.

For detailed information, see the official GraphQL documentation and GraphQL specification.

What is GraphQL?

GraphQL is a query language for APIs. The key idea behind it is that you specify exactly what data you need, and the API returns only that data. This is different from REST APIs, where each endpoint returns a fixed structure, and you often get more data than you need (or have to make multiple requests to get everything you need).

How GraphQL differs from REST

If you've used REST APIs, you're familiar with this pattern:

  • Different endpoints for different resources (/devices, /devices/123, /assets)

  • HTTP methods determine the action (GET to read, POST to create, PUT to update, DELETE to remove)

  • Each endpoint returns a predefined data structure

GraphQL works differently:

Aspect
REST
GraphQL

Endpoints

Multiple (/devices, /assets, /users)

Single (/graphql)

Data returned

Fixed structure per endpoint

You specify exactly what fields you want

Multiple resources

Multiple requests needed

One request can fetch related data

HTTP methods

GET, POST, PUT, DELETE

Always POST (with query or mutation inside)

Sample request

Imagine you need to display a device's title, its status name, and the workspace it belongs to.

With REST, you might need multiple requests:

With GraphQL, you make one request and specify exactly what you need:

The response contains only what you asked for:

Single endpoint

Navixy GraphQL API has one endpoint: https://api.eu.navixy.com/v4/graphql.

Every query and mutation goes to the same URL, and the request body tells the API what you want to do. A subscription uses the same URL over server-sent events, or a WebSocket connection with its own path. See Subscriptions.

Product namespaces

Navixy GraphQL API serves more than one product through this endpoint, and each product has its own entry point. All Business Data Repository operations are nested under the bdr field, so every query and mutation in these docs starts the same way:

The response mirrors the same structure: your data sits under data.bdr.

The bdr block sets one rule for mutations: a single block may select only one mutation field. To run several mutations in one request, alias the bdr field itself, one block per call. See Batch mutations. Queries have no such rule: one bdr block can select as many query fields as you need.

Two lookup queries stay at the top level, outside any product namespace: node and nodes. They return a record of any product by its ID. See Identifiers.

Identifiers

Every record of the API has an id of the type ID, which is a string. All products use one ID format, so an ID never repeats across products. Because IDs never repeat, the top-level node query can return a record of any product:

The ... on Device { } block is an inline fragment: it selects title only when the record is a device. The response has the device, or null when the API can't return it:

node returns null for an ID that doesn't exist, that belongs to another workspace, or that names a record your role can't read. nodes(ids: [...]) does the same for a list: it returns null in place of each ID it can't return. Lookups inside a product, such as bdr { device(id: ...) }, also return null for these IDs. The one difference: when your roles allow no read at all on the kind of record, the lookup inside a product returns a PERMISSION_DENIED error instead.

Some IDs work differently with node and with ID arguments:

  • node returns a Console organization only when it's one of your own organizations: the organizations that console { accessibleOrganizations } lists.

  • The IDs of Console workspace operations and of tracking commands don't identify records of this API. node returns null for them.

  • An argument that expects the ID of one kind of record returns a VALIDATION_ERROR for the ID of another kind. For example, device(id:) returns this error for an asset ID.

Treat an ID as a string that you store and send back unchanged. Don't build IDs, and don't read meaning from their characters.

A few fields have the type UUID instead of ID. Their values don't identify records of this API. Examples are actorId, the ID of the user or integration that made a change, and idempotencyKey, a key that you choose yourself. node can't return a record for these values.

Operations

GraphQL supports three types of operations:

Operation
Purpose
Similar to REST

Query

Read data

GET

Mutation

Create, update, or delete data

POST, PUT, DELETE

Subscription

Receive real-time updates

No REST equivalent

Queries

A query retrieves data without changing anything. It's the GraphQL equivalent of a REST GET request.

Here's a simple query that fetches a device:

Let's break this down:

  • query tells GraphQL you want to read data

  • device is the name of the query (what you're fetching)

  • (id: "550e8400-...") is an argument that specifies which device

  • { id title } lists the fields you want returned

The response mirrors your query structure:

Notice that the response contains only id and title, exactly what you requested. If you also need the device's status, add it to your query:

The status field returns an object, so you specify which fields of that object you want (code and title).

Mutations

A mutation modifies data, creating, updating, or deleting something. It combines what REST does with POST, PUT, and DELETE into one operation type.

Here's a mutation that updates a device's title:

Let's break this down:

  • mutation tells GraphQL you want to change data

  • deviceUpdate is the name of the mutation

  • input: { ... } provides the data for the update, including the current version for optimistic locking

  • { device { ... } } specifies what to return after the update

The response shows the updated data:

This is useful because you can confirm the change and get the new version number without making a separate query.

Here's a mutation that creates a new asset:

Subscriptions

A subscription keeps a connection open and receives data every time it changes. A query returns one response and ends. A subscription returns a new message on the same connection whenever the data changes. Your app doesn't have to send the same query again and again.

Navixy GraphQL API has one subscription, trackingAssetStates. It sends the state of assets every time their devices report.

Here's a subscription that follows the connectivity of every asset in your workspace:

Let's break this down:

  • subscription tells GraphQL that you want to receive updates

  • trackingAssetStates is the name of the subscription

  • input: { ... } sets what to follow, here with no readings

  • { changes { ... } } lists the fields of every message

Every message has the same shape as a query response. The API first sends the current state of every asset that has a linked device, then a new message whenever devices report:

A subscription isn't sent as a plain POST request that gets one response. The API accepts it over two transports:

  • A WebSocket connection with the graphql-transport-ws protocol, at wss://api.eu.navixy.com/v4/graphql/ws. The WebSocket accepts subscriptions only.

  • Server-sent events: a POST request to the usual endpoint with the Accept: text/event-stream header. The response stays open and delivers each message as an event.

These rules apply to every WebSocket connection. They use the message types of the graphql-transport-ws protocol. The client sends connection_init to authenticate and subscribe to start a subscription. The server sends next with data, and complete when a subscription ends.

  • The client must send the access token in the payload of connection_init, as {"Authorization": "Bearer <ACCESS_TOKEN>"}, within 10 seconds of opening the connection. Otherwise the server closes the connection with code 4408. The server closes the connection with code 4401 when connection_init has no valid token.

  • The server sends a ping every 15 seconds and closes a connection whose client has sent nothing for 45 seconds, with code 1001. Client libraries such as graphql-ws reply to every ping with a pong message automatically.

  • A query or a mutation sent over the WebSocket gets one next message with a VALIDATION_ERROR error, then complete. A second subscribe with an ID that is already in use closes the connection with code 4409.

  • One caller can keep up to 12 connections open. The server closes a connection over that number with code 4403 after its connection_init. graphql-ws then reconnects after a delay that grows with each attempt.

  • Every subscription that you send counts as one request toward your request rate, even one that the server then refuses. A subscription over the limit gets one next message with a RATE_LIMITED error, then complete, and the connection stays open.

  • A subscription ends with an UNAUTHORIZED error when its access token expires. Get a new token and subscribe again.

Subscription limits lists every limit with its default value. Building a live fleet map shows how to connect, authenticate, and handle errors on both transports. State streams documents every field of the subscription.

Fields

Fields are the pieces of data you can request from an object. When you write a query, you're selecting which fields to include in the response.

Every object type in the API has a defined set of fields. For example, a Device has fields like:

  • id: the unique identifier

  • title: the display name

  • status: the current status (which is itself an object with its own fields)

  • workspace: the workspace that owns it

  • identifiers: the hardware identifiers, such as IMEI or serial number

You only get the fields you ask for. If you don't need identifiers, don't include it in your query, and it won't be in the response.

Nested fields

Some fields return objects rather than simple values. When a field returns an object, you must specify which fields of that object you want.

Here, status and workspace are objects, so their fields go inside curly braces. The title and code fields return simple values (strings), so they don't need nested fields. isActive returns a boolean.

Arguments

Arguments allow you to filter or configure what data a query returns. You pass them in parentheses after the field name.

The device query requires an id argument to know which device to fetch:

The devices query (plural) can take optional arguments to filter results:

This requests the first 10 devices in your workspace with a specific status. Every argument here is optional: filter narrows the list, and first sets the page size.

Types

Every field in GraphQL has a type that defines what kind of data it holds. It can be predefined, like string or boolean, or API-specific, like DeviceIdType. Understanding types helps you read the API reference and write correct queries.

Scalar types

Scalars are primitive values, the basic building blocks. When calling a GraphQL API, you must specify nested subfields until you return only scalars.

GraphQL has the following built-in scalar types:

Type
Description
Example

String

Text

"Truck 42"

Int

Whole number

42

Float

Decimal number

3.14

Boolean

True or false

true

ID

Unique identifier (serialized as string)

"019a6b2f-793e-807b-8001-444556430011"

Navixy GraphQL API adds these custom scalar types:

Type
Description
Example

DateTime

Date and time with timezone (ISO 8601)

"2025-01-15T14:30:00.000Z"

Date

Date without time (ISO 8601)

"2025-01-15"

JSON

Arbitrary JSON data

{"color": "red", "priority": 1}

Code

Machine-readable identifier: letters, digits, _, ., and -, starting with a letter or digit, up to 100 characters. Case is ignored when codes are compared.

"truck", "device_type"

HexColorCode

Color in hexadecimal format

"#FF5733"

Object types

Object types represent the things you can fetch from the API: devices, workspaces, assets, users, and so on. Each object type defines a set of fields you can request.

Here's what the Device type looks like in the schema:

This tells you:

  • Device is an object type

  • It has fields like id, version, title, status, etc.

  • Each field has its own type (ID!, Int!, String!, DeviceStatus!)

Some fields return scalar types (id returns ID!), while others return other object types (status returns DeviceStatus!). When a field returns an object type, you must specify which of its fields you want:

In the API reference, you'll see object types like Device, Workspace, DeviceStatus, and many others.

Enums

Enums are fixed sets of values. Unlike strings, you can only use the predefined values.

For example, OrderDirection is an enum with these values:

  • ASC

  • DESC

When using an enum in a query, write the value without quotes:

Input types

Input types are used for mutation arguments. They look similar to object types but can only be used as inputs, not outputs.

In the API reference, you'll see input types like DeviceCreateInput, AssetUpdateInput, and DeviceFilter. They define what fields you can (or must) provide:

Interfaces

Interfaces define a set of fields shared by multiple types. If a type implements an interface, it guarantees those fields exist.

For example, the Node interface requires the id field. Many types implement it: Device, Asset, Workspace, and others. This means you can always request id from any of these types.

The Versioned interface provides the version field, so you can always request version on types that implement it and include it when you update them. See Optimistic locking for how the field is used.

Type syntax

The API reference uses special notation to indicate whether fields are required and whether they return single values or lists.

Required vs nullable

The ! symbol means a value is required (non-null):

Syntax
Meaning

String

May be null

String!

Never null: always returns a string

For arguments, ! means you must provide a value:

Arrays

Square brackets [] indicate a list of values:

Syntax
Meaning

[String]

A list of strings (list and items may be null)

[String!]

A list of non-null strings (list may be null)

[String]!

A non-null list of strings (items may be null)

[String!]!

A non-null list of non-null strings

The most common pattern is [Type!]!: a guaranteed list where every item exists:

Reading complex types

When you see a type like [DeviceEdge!]! in the API reference:

  1. Start from the inside: DeviceEdge, which is an object type

  2. Add !: DeviceEdge!: each item is never null

  3. Add []: [DeviceEdge!]: it's a list

  4. Add outer !: [DeviceEdge!]!: the list itself is never null

So [DeviceEdge!]! means "you'll always get a list, and every item in that list will be a valid DeviceEdge object."

How responses work

GraphQL responses are JSON objects with a consistent structure.

Successful responses

A successful response has a data field containing your requested data:

The structure inside data matches your query structure exactly.

Error responses

If something goes wrong, the response includes an errors array:

The extensions object contains machine-readable information for handling the error in your code. See Error handling for more information.

Schema

A GraphQL API has a schema that defines all available types, fields, queries, and mutations. The schema is like a contract: it tells you exactly what you can request and what you'll get back.

The Navixy GraphQL API schema is public and available to developers, split into one file for the shared types and one per product.

Introspection

GraphQL APIs are self-describing. You can explore the schema using GraphQL tools or by querying the API directly. This is called introspection. GraphQL has special built-in fields that start with __ (double underscore) for this purpose.

For example, to see all fields available on the Device type:

See also

Last updated

Was this helpful?