Making HTTP Requests
Learn how to make HTTP requests with Apollo Connectors
In this guide, you'll learn about:
The API requirements for connector compatibility
How to make HTTP requests with connectors
Connectors error handling
API requirements for connector compatibility
Connectors make it easy to build a GraphQL API using your existing HTTP/JSON APIs. They're flexible and work with most APIs, especially if they conform to the following guidelines.
Content Type
Connectors expect a JSON response body. If your API endpoint doesn't default to a JSON content type, you may need to specify an Accept: application/json
header using the mechanisms described below.
Some APIs may have a different mechanism for specifying the response content type, such as a query parameter or file extension. Ensure your connector is configured appropriately to request a JSON response.
Content-Type
response header. By default, they interpret any response body as JSON.Core JSON-over-HTTP requirements
Connectors work with APIs that follow these JSON-over-HTTP principles:
Your endpoints accept requests using these HTTP verbs:
GET
,POST
,PUT
,PATCH
,DELETE
.Your endpoints use path segments and query string parameters for inputs, such as
/users/123
or/users?limit=10
.For
POST
,PUT
, andPATCH
requests with request bodies, your endpoints accept JSON.Your endpoints respond with JSON values—typically objects, but any JSON value is allowed.
Your endpoints always return a status code between 200 and 299 for successful requests.
Your endpoints provide a known set of properties. GraphQL doesn't have a
Map
type, so you must define the mapping between JSON properties and GraphQL fields in advance. (You can map an arbitrary map to a scalar field using a custom scalar type.)
Graph-like conventions
Connectors work best with APIs that follow these graph-like conventions for representing and retrieving entities:
You represent your entities across various endpoints using the same identifiers. For example, a
User
is consistently identified by123
across all endpoints.You provide endpoints to fetch an entity by its primary key. For example,
/users/123
returns the user with ID123
.Your endpoints use simple values for foreign keys. For example, a
User
object has acompanyId
field containing the ID of the company it belongs to. (Using a full URL such as{"company": "http://myapi.com/company/234"}
is difficult to work with).When appropriate, your endpoints embed related entities in the response. For example, a
User
object might include acompany
field that contains aCompany
object.
Security
To ensure secure interactions, your API should follow these security best practices.
Your endpoints perform their own authentication and authorization checks as necessary. You can add layers of additional security using GraphOS Router's security features.
Your endpoints accept authentication information in request headers. The headers can come directly from the client or be injected by the router.
Your endpoints don't use query parameters for sensitive information. The router emits full URLs in logs and traces.
Making HTTP requests
This section covers the essentials of making HTTP requests using connectors. It includes examples specifying the HTTP method and URL, setting headers, and defining request bodies.
HTTP Methods
You specify the HTTP method for your request in the http
argument of a @connect
directive. You must specify one of the following HTTP methods: GET
, POST
, PUT
, PATCH
, or DELETE
, followed by the URL.
Example GET request
1type Query {
2 products: [Product]
3 @connect(
4 http: { GET: "https://myapi.dev/products" }
5 selection: "id"
6 )
7}
Example POST request
1type Mutation {
2 createProduct(name: String!): Product
3 @connect(
4 http: {
5 POST: "https://myapi.dev/products"
6 body: "name: $args.name"
7 }
8 selection: "id"
9 )
10}
Example PUT request
1type Mutation {
2 setProduct(id: ID!, name: String!): Product
3 @connect(
4 http: {
5 PUT: "https://myapi.dev/products/{$args.id}"
6 body: "name: $args.name"
7 }
8 selection: "id"
9 )
10}
Example PATCH request
1type Mutation {
2 updateProduct(id: ID!, name: String!): Product
3 @connect(
4 http: { PATCH: "https://myapi.dev/products/{$args.id}?name={$args.name}" }
5 selection: "id"
6 )
7}
Example DELETE request
1type Mutation {
2 deleteProduct(id: ID!): Product
3 @connect(
4 http: { DELETE: "https://myapi.dev/products/{$args.id}" }
5 selection: "id"
6 )
7}
Dynamic URLs
URLs can be dynamic and use values from field arguments, sibling fields, and router configuration. The dynamic parts appear between curly braces ({}
). For example, you can use field arguments as path segments:
1type Query {
2 product(storeId: ID!, productId: ID!): Product
3 @connect(
4 http: {
5 GET: "https://myapi.dev/store/{$args.storeId}/products/{$args.productId}"
6 }
7 selection: "id"
8 )
9}
Path segment values are URL-encoded. If the value contains a /
, it is encoded as %2F
.
Query parameters
You add query parameters to the URL by appending them with a ?
and separating them with &
. You can use field arguments to create query parameters:
1type Query {
2 products(limit: Int = 10, offset: Int): Product
3 @connect(
4 http: {
5 GET: "https://myapi.dev/products?limit={$args.limit}&offset={$args.offset}"
6 }
7 selection: "id"
8 )
9}
When a value is missing or null
, the result is an empty string. In the previous example, if offset
in {$args.offset}
is not provided, the URI will end with &offset=
. Parameter names still appear in the URI if the parameter value is missing or null
.
Headers
You add headers to your HTTP requests using the http.headers
argument. Like with URIs, you can define header values using a combination of fixed values and dynamic
mapping expressions in curly braces ({}
).
The following example uses a static value for the x-api-version
header and a dynamic value with the $config
variable for the Authorization
header:
1type Query {
2 products: [Product]
3 @connect(
4 http: {
5 GET: "https://myapi.dev/products"
6 headers: [
7 { name: "x-api-version", value: "2024-01-01" }
8 { name: "Authorization", value: "Bearer {$config.token}" }
9 ]
10 }
11 selection: "id"
12 )
13}
You can also propagate headers from the incoming client request using the from
argument:
1type Query {
2 products: [Product]
3 @connect(
4 http: {
5 GET: "https://myapi.dev/products"
6 headers: [{ name: "Authorization", from: "Authorization" }]
7 }
8 selection: "id"
9 )
10}
Sharing configuration with @source
You can use the @source
directive to share a partial URL and headers with multiple connectors. To use a @source
:
Import the
@source
directive from the connectors@link
.Apply the
@source
directive to theschema
.Define a
baseURL
.Optionally define
headers
, which can't contain$this
or$args
.
Set the
source:
in each@connect
directive that should use the shared configuration.Use a partial URL in the
@connect
directive containing only the path and query parameters (no scheme, host, etc.).Define headers in
@connect
to override headers from the@source
with the samename
.
The connector request below resolves to https://myapi.example.com/v1/products?client=router&first=10
. It includes the X-API-Key
header from the @source
configuration and the X-Product-Type
header from the @connect
configuration.
1extend schema
2 @link(
3 url: "https://specs.apollo.dev/connect/v0.1"
4 import: ["@connect", "@source"]
5 )
6 @source(
7 name: "myapi"
8 http: {
9 baseURL: "https://myapi.example.com/v1?client=router"
10 headers: [{ name: "X-API-Key", value: "{$config.api_key}" }]
11 }
12 )
13
14type Query {
15 products(first: Int = 10): [Product]
16 @connect(
17 source: "myapi"
18 http: {
19 GET: "/products?first={$args.first}"
20 headers: [{ name: "X-Product-Type", from: "Product-Type" }]
21 }
22 selection: "id"
23 )
24}
Request body
The http.body
field defines a JSON body to send with the request using the mapping language.
1type Mutation {
2 createProduct(input: CreateProductInput!): Product
3 @connect(
4 http: {
5 POST: "https://myapi.dev/products"
6 body: """
7 $args.input {
8 name
9 price
10 }
11 """
12 }
13 selection: "id"
14 )
15}
Form URL encoding
By adding a Content-Type
header of exactly application/x-www-form-urlencoded
, GraphOS Router encodes the request body as a form URL encoded string.
1type Mutation {
2 createPost(input: CreatePostInput!): Post
3 @connect(
4 http: {
5 POST: "https://api.example.com/posts"
6 headers: [
7 { name: "Content-Type", value: "application/x-www-form-urlencoded" }
8 ]
9 }
10 selection: """
11 $args.input {
12 title
13 content
14 }
15 """
16 )
17}
The router first maps the request body to a JSON object:
{
"title": "Hello, world!",
"content": "This is a post."
}
Then, it encodes the object as a x-www-form-urlencoded
string:
title=Hello%2C+world%21&content=This+is+a+post.
Form URL encoding details
- List values are indexed starting from 0 using the
list[0]=value
syntax. - Nested objects use the
parent[child]=value
syntax. - Spaces are encoded as
+
.
1type Mutation {
2 example(input: ExampleInput!): Example
3 @connect(
4 http: {
5 POST: "/example"
6 headers: [
7 { name: "content-type", value: "application/x-www-form-urlencoded" }
8 ]
9 }
10 selection: """
11 $args.input {
12 name
13 tags
14 addresses {
15 street
16 city
17 state
18 zip
19 }
20 }
21 """
22 )
23}
24
25input ExampleInput {
26 name: String!
27 tags: [String!]
28 addresses: [AddressInput!]
29}
30
31input AddressInput {
32 street: String!
33 city: String!
34 state: String!
35 zip: String!
36}
1name=Example
2&tags[0]=tag1
3&tags[1]=tag2
4&addresses[0][street]=123+Main+St
5&addresses[0][city]=Anytown
6&addresses[0][state]=CA
7&addresses[0][zip]=12345
8&addresses[1][street]=456+Elm+St
9&addresses[1][city]=Othertown
10&addresses[1][state]=NY
11&addresses[1][zip]=54321
Error handling
Following the GraphQL specification, if a connector returns an error, the corresponding field in the response's data
is null
, and the error is logged in the errors
array. Connectors follow this convention for all non-20x responses.
See the common errors section on the troubleshooting page for information on composition errors.