Skip to content

Getting started

You need Docker (with the compose plugin) and a clone of the repository, for the example.

1. Get the base image

docker pull ghcr.io/abusi/gqlgw:0.2.0     # linux/amd64, linux/arm64
make image            # docker build -t gqlgw:<version> -t gqlgw:latest .

The image is built from the official openresty/openresty:1.31.1.1-resolute. It adds gqlc, libgql.so and the Lua modules. It holds no schema: a gateway is an image built from it.

2. Run the example gateway

examples/docker is a blog API served by two REST services. A mock backend plays both services, and memcached is the shared cache.

git clone https://github.com/abusi/gqlgw && cd gqlgw/examples/docker
GQLGW_IMAGE=ghcr.io/abusi/gqlgw:0.2.0 docker compose up --build   # or, after make image: docker compose up --build
flowchart LR
    you([curl]) -- ":8080 /graphql" --> gw[gateway]
    you -- ":8191 /gqlgw_metrics" --> gw
    gw -- "users + posts" --> be["backend (mock)"]
    gw <-.-> mc[(memcached)]

Query it:

curl -s localhost:8080/graphql -H 'Content-Type: application/json' \
     -d '{"query":"{ posts(limit: 3) { title author { name } } }"}'
{"data":{"posts":[{"title":"Post 1","author":{"name":"User 2"}},{"title":"Post 2","author":{"name":"User 3"}},{"title":"Post 3","author":{"name":"User 4"}}]}}

The example turns on the debug headers (GQLGW_DEBUG_HEADERS=true), which show what happened behind the scenes. Add -i to the curl command to see them:

X-GQL-Rounds: 2                          # two rounds: posts, then their authors
X-GQL-Subrequests: 2                     # two upstream calls
X-GQL-Batches: 1                         # the three authors in one batch call
X-GQL-Cache: l1=0;l2=0;l3=0;miss=3       # three author lookups, none cached yet
X-GQL-Doc-Cache: miss                    # the query text was parsed and validated

Run it again: X-GQL-Subrequests: 1. The authors now come from the cache, which is shared through memcached by every gateway that uses it.

Prometheus metrics are on the admin port:

curl -s localhost:8191/gqlgw_metrics | grep gqlgw_requests_total

3. Build your own gateway

A gateway is three files: your schema, your gqlgw.toml, and a three-line Dockerfile.

# A gateway image: your schema and configuration on top of gqlgw.
#
#   docker build -t my-gateway .
#   docker run -p 8080:8080 -p 8191:8191 \
#       -e USERS_URL=http://users:8080 -e POSTS_URL=http://posts:8080 my-gateway
# the base image: gqlgw:latest (make image), or a release: ghcr.io/abusi/gqlgw:<version>
ARG GQLGW_IMAGE=gqlgw:latest
FROM ${GQLGW_IMAGE}
COPY schema.graphqls gqlgw.toml /etc/gqlgw/
# the schema is compiled, and the configuration checked, when the image is built
RUN gqlc compile -c /etc/gqlgw/gqlgw.toml /etc/gqlgw/schema.graphqls
# gqlgw configuration (TOML 1.1).
#
# String values may use ${VAR}, ${VAR:-default} and ${VAR:?message}: they are
# read from the environment when the container starts (gqlc render).

[gateway]
listen = 8080
access_log = "/dev/stdout"
debug_headers = "${GQLGW_DEBUG_HEADERS:-false}"

[cache]
shm_size = "64m"
l2_ttl = 5

[cache.memcached]
host = "${MEMCACHED_HOST:-memcached}"
port = "${MEMCACHED_PORT:-11211}"

[metrics]
port = 8191
exclude_operations = ["^IntrospectionQuery$"]

# one section per @fetch(source: "...") of the schema
[sources.users]
url = "${USERS_URL:?set USERS_URL to the users service, for example http://users:8080}"

[sources.posts]
url = "${POSTS_URL:?set POSTS_URL to the posts service, for example http://posts:8080}"
forward_headers = ["Authorization", "Accept-Language"]
"""
Example schema: a blog served by two REST services (users, posts).
"""
schema {
  query: Query
  mutation: Mutation
}

type Query {
  "A user by id."
  user(id: ID!): User @fetch(source: "users", path: "/users/{args.id}")

  "Latest posts."
  posts(limit: Int = 20, tag: String): [Post!]!
    @fetch(source: "posts", path: "/posts?limit={args.limit}&tag={args.tag}")

  "Search anything."
  search(q: String!): [SearchResult!]! @fetch(source: "posts", path: "/search?q={args.q}", select: "results")
}

type Mutation {
  renameUser(id: ID!, name: String!): User
    @fetch(source: "users", method: "PATCH", path: "/users/{args.id}", body: "{\"name\": {args.name}}")
  createPost(input: PostInput!): Post
    @fetch(source: "posts", method: "POST", path: "/posts", body: "{args.input}")
}

interface Node {
  id: ID!
}

type User implements Node @cache(ttl: 300) {
  id: ID!
  name: String!
  email: String @deprecated(reason: "Use contact.email")
  createdAt: String @from(key: "created_at")
  posts: [Post!]! @fetch(source: "posts", path: "/posts?author={parent.id}")
}

type Post implements Node {
  id: ID!
  title: String!
  body: String
  tags: [String!]!
  authorId: ID! @from(key: "author_id")
  author: User!
    @fetch(source: "users", path: "/users/{parent.authorId}")
    @batch(path: "/users?ids={keys}", match: "id", max: 50)
}

union SearchResult @discriminator(key: "kind") = User | Post

input PostInput {
  title: String!
  body: String
  tags: [String!] = []
  status: Status = DRAFT
}

enum Status {
  DRAFT
  PUBLISHED
  ARCHIVED @deprecated
}
docker build --build-arg GQLGW_IMAGE=ghcr.io/abusi/gqlgw:0.2.0 -t my-gateway .
docker run --read-only --tmpfs /tmp -p 8080:8080 -p 8191:8191 \
    -e USERS_URL=http://users:8080 -e POSTS_URL=http://posts:8080 my-gateway

In your own repository, write the base image directly: FROM ghcr.io/abusi/gqlgw:0.2.0.

What happens at each step:

flowchart LR
    subgraph build ["docker build"]
        sdl[schema.graphqls] --> compile["gqlc compile -c"]
        toml[gqlgw.toml] --> compile
        compile --> gqlb["schema.gqlb<br/>schema.routes.lua"]
    end
    subgraph start ["docker run"]
        env(["environment<br/>USERS_URL, ..."]) --> render["gqlc render"]
        render --> conf["gqlgw.conf<br/>gqlgw.lua"]
        conf --> t["openresty -t"] --> run["openresty"]
    end
    gqlb --> render
  • An invalid schema, or a @fetch(source: "x") without a [sources.x], fails docker build.
  • A missing variable (${USERS_URL:?...}) stops the container at start, with the message written in the TOML.

Next steps