- Overview
- Prerequisites
- Step 1: Project Initialisation
- Step 2: Create Schema Entities for REST data
- Step 3: Create Your Provider
- What is urlToIdTransform?
- Other Common Configurations
- Providing Auth Headers
- Changing Options per Endpoint
- Result Key
- Fetch Method for Relationships
- Additional Extension Ideas
- Conclusion
Overview
This guide will walk you through the process of connecting Graphweaver to a REST API. We'll use a sample REST API for illustration purposes, but you can adapt the steps to connect to your own API. An example of a REST implementation can be found in our Graphweaver Repo.
Prerequisites
Before you begin, ensure that you have the following prerequisites:
- Node.js 18 or greater installed
pnpm
version 8 or greater installed
Step 1: Project Initialisation
Create a new Graphweaver project by running the following command:
npx graphweaver@latest init
Follow the prompts to set up your project. Provide a name when prompted, and select the REST API when asked about Graphweaver backends.
Install the dependencies in your project.
cd your-project-name
pnpm i
Ensure your graphweaver instance starts with:
pnpm start
Step 2: Create Schema Entities for REST data
We need to add the entity to the API by defining what fields come back on the result objects and telling Graphweaver how to get the data. We also need to tell Graphweaver how to get the IDs back for any relationships in our data. In this example below, a Person
can have 0 or more Vehicle
entities linked to them.
backend/schema/person.ts
import { Entity, Field, ID, RelationshipField } from '@exogee/graphweaver';
import { RestBackendProvider } from '@exogee/graphweaver-rest';
import { urlToIdTransform } from '../utils';
import { Vehicle } from './vehicle';
@Entity('Person', {
// Our API does not support write operations
adminUIOptions: { readonly: true },
apiOptions: { excludeFromBuiltInWriteOperations: true },
})
export class Person {
@Field(() => ID, { primaryKeyField: true })
url!: string;
@Field(() => String)
name!: string;
@Field(() => Number)
height!: number;
@Field(() => Number)
mass!: number;
@Field(() => String)
hair_color!: string;
@Field(() => String)
birth_year!: string;
@RelationshipField(() => [Vehicle], { id: 'vehicles', nullable: true })
vehicles!: Vehicle[];
}
Step 3: Create Your Provider
A provider tells Graphweaver how to fetch the data. We’ll go ahead and add ours to the options on the @Entity
decorator:
@Entity('Person', {
// Our API does not support write operations
adminUIOptions: { readonly: true },
apiOptions: { excludeFromBuiltInWriteOperations: true },
provider: new RestBackendProvider({
baseUrl: 'https://swapi.info/api',
defaultPath: 'people',
fieldConfig: {
url: { transform: urlToIdTransform },
vehicles: { transform: urlToIdTransform },
},
}),
})
What is urlToIdTransform
?
In the swapi.info API, all references between entities are full URLs, for example:
{
"url": "https://swapi.info/api/people/1",
"name": "Luke Skywalker",
"homeworld": "https://swapi.info/api/planets/1",
"vehicles": [
"https://swapi.info/api/vehicles/14",
"https://swapi.info/api/vehicles/30"
]
// Other fields omitted for brevity
}
Graphweaver needs to know what the actual IDs are for these entities so that when it goes to apply RESTful API standards to our data it can construct correct URLs. Here’s the implementation of urlToIdTransform
:
const extractIdFromUrl = (url: unknown) => {
if (typeof url !== 'string') return url;
return new URL(url).pathname.split('/')[3];
};
export const urlToIdTransform: FieldTransformer = {
fromApi: (value: unknown) => {
if (Array.isArray(value)) return value.map(extractIdFromUrl);
return extractIdFromUrl(value);
},
// Since the whole API is read only, this will never get used in this example. If our API allowed
// write operations, we'd need to implement the inverse transform here and return a well-formed URL.
toApi: (id: string) => id,
};
This allows us to transform the data on the way in from the API so that all the url
fields just become IDs or arrays of IDs.
We provide this transform for the url
and vehicles
fields so that these values get normalised to just IDs.
Other Common Configurations
Providing Auth Headers
Another common task when connecting with REST APIs is providing additional headers. You can use the clientOptions
field on the RestBackendProvider
constructor.
Changing Options per Endpoint
By default the RestBackendProvider
will assume your API follows standard REST conventions, e.g. for a path of people
, it would assume the following pattern:
- List:
GET /people
- Get One:
GET /people/[id]
- Create:
POST /people
- Update:
PUT /people/[id]
- Delete:
DELETE /people/[id]
What if your API doesn’t conform to these conventions? This is what the endpointOverrides
option does. You can override the HTTP verb or path used for any of our operations like so:
const provider = new RestBackendProvider({
// ...Other options...
endpointOverrides: {
create: {
method: 'PUT',
path: 'create-person'
},
delete: {
path: 'delete-person'
}
}
});
The operations you can provide overrides for are:
create
list
getOne
update
delete
Result Key
By default we assume that the result of an operation is the data, for example in a List operation, we expect the result to be an array with the objects in it like so:
[
{
"name": "Luke Skywalker"
}
]
But what if your API has metadata on the response and looks more like this?
{
"next_token": "abc123",
"count": 57,
"results": [
{
"name": "Luke Skywalker"
}
]
}
The option to handle this is called a resultKey
. In this example, we’d set the resultKey
to results
, and Graphweaver will pull this out of the result object before proceeding as normal. If your API is consistent about this, you can just use the defaultResultKey
option like so:
const provider = new RestBackendProvider({
// ...Other options...
defaultResultKey: 'results'
});
If on the other hand, every operation has a different result key, you can use the resultKey
configuration on the endpointOverrides
option. See for more info.
Fetch Method for Relationships
REST does not have a built in understanding of how to filter entities. It is able to list them or get one. This leaves us with a bit of a challenge with large datasets. We can either:
- List the whole entity then filter the results on our side in memory, or
- Take the IDs we have and individually get them.
Our default is to take approach #1, and it is a sane default because it only results in one GET
to the other API for each relationship it loads, but this doesn’t always work. With larger data sets you’ll need to fall back to individually fetching the matching rows. To do this, you can use the relationshipLoadingMethod
option on the RestBackendProvider
constructor:
const provider = new RestBackendProvider({
// ...Other options...
relationshipLoadingMethod: 'findOne'
});
Additional Extension Ideas
Ideas we’re interested in implementing, but haven’t done yet:
- Automatic pagination, e.g. for our in memory filtering, if your API returns multiple pages and you want to automatically unroll them, we could do this for you.
- Server side filtering: Providing a way for developers to specify how their server filters entities, which we’d then lean on the same way we do for database providers, bypassing in-memory filtering entirely.
If any of these ideas are interesting to you, reach out on Slack! We’re looking for more concrete examples to be able to implement an API that will work in the real world for these features.
Conclusion
Congratulations! You've successfully connected Graphweaver to a REST API. You can now explore and interact with the data from your API using GraphQL queries.