Skip to content

suprbdev/restql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

restql

API gateway implementing the Strangler Fig pattern. Receives legacy REST requests, maps them to GraphQL variables, executes against a GraphQL endpoint, and maps the response back to a legacy REST JSON structure.

REST client → restql → GraphQL API → restql → REST client

Using restql in your project

Requirements

  • Docker
  • Bun (for local type checking)

1. Set up Docker Compose

compose.yml

services:
  restql:
    image: suprbdev/restql:latest
    restart: unless-stopped

compose.override.yml

networks:
  proxy:
    external: true

services:
  restql:
    networks:
      - proxy
      - default
    environment:
      GRAPHQL_ENDPOINT: "http://your-graphql-service:5000/graphql"
      VIRTUAL_HOST: "myproject.localhost"
      VIRTUAL_PORT: "3000"
    volumes:
      - ./routes:/app/routes
      - ./mappings:/app/mappings

2. Install the package

Install @suprbdev/restql locally so your editor and LSP can resolve types:

bun add @suprbdev/restql graphql-request graphql

3. Add a tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "types": ["bun-types"]
  },
  "include": ["routes/**/*", "mappings/**/*"]
}

4. Create a mapping

mappings/product.ts — define the GraphQL response shape and how to transform it to your legacy format:

export interface GqlProduct {
  id: string;
  name: string;
  priceInCents: number;
  createdAt: string;
}

export interface LegacyProduct {
  id: string;
  name: string;
  price: number;
  created_at: string;
}

export function mapBaseProduct(p: GqlProduct): LegacyProduct {
  return {
    id: p.id,
    name: p.name,
    price: p.priceInCents / 100,
    created_at: p.createdAt,
  };
}

5. Create a route

routes/product.ts — define endpoints as a config array. Each entry is one HTTP endpoint:

import { gql } from "graphql-request";
import { defineRoutes } from "@suprbdev/restql";
import { mapBaseProduct, type GqlProduct } from "../mappings/product.js";

interface ListProductsResponse {
  products: { nodes: GqlProduct[] };
}

interface GetProductResponse {
  productById: GqlProduct | null;
}

export default defineRoutes([
  {
    method: "GET",
    path: "/products",
    query: gql`
      query ListProducts($first: Int = 20) {
        products(first: $first) {
          nodes { id name priceInCents createdAt }
        }
      }
    `,
    schema: {
      querystring: {
        type: "object",
        properties: {
          limit: { type: "integer", minimum: 1, maximum: 100, default: 20 },
        },
      },
    },
    vars: (req) => ({ first: req.query["limit"] ?? 20 }),
    transform: (data: ListProductsResponse) => ({
      data: data.products.nodes.map(mapBaseProduct),
      total: data.products.nodes.length,
    }),
  },
  {
    method: "GET",
    path: "/products/:id",
    query: gql`
      query GetProduct($id: UUID!) {
        productById(id: $id) { id name priceInCents createdAt }
      }
    `,
    schema: {
      params: {
        type: "object",
        required: ["id"],
        properties: { id: { type: "string", format: "uuid" } },
      },
    },
    vars: (req) => ({ id: req.params["id"] }),
    notFound: (data: GetProductResponse) => !data.productById,
    transform: (data: GetProductResponse) => ({ data: mapBaseProduct(data.productById!) }),
  },
  {
    method: "POST",
    path: "/products",
    query: gql`
      mutation CreateProduct($input: CreateProductInput!) {
        createProduct(input: $input) {
          product { id name priceInCents createdAt }
        }
      }
    `,
    schema: {
      body: {
        type: "object",
        required: ["name", "price"],
        properties: {
          name: { type: "string" },
          price: { type: "number" },
        },
      },
    },
    vars: (req) => ({
      input: {
        product: {
          name: req.body["name"],
          priceInCents: Math.round((req.body["price"] as number) * 100),
        },
      },
    }),
    transform: (data: { createProduct: { product: GqlProduct } }) => ({
      data: mapBaseProduct(data.createProduct.product),
    }),
  },
]);

6. Start the gateway

docker compose up -d

Routes are picked up automatically. Restart the container after adding new route files.

Project structure

my-project/
├── routes/
│   └── product.ts
├── mappings/
│   └── product.ts
├── package.json
├── tsconfig.json
├── compose.yml
└── compose.override.yml

Route files can be nested for organisation — all files under routes/ are loaded recursively:

routes/
├── product.ts         → /products, /products/:id
└── admin/
    └── report.ts      → /admin/reports

Endpoint config reference

Field Required Description
method yes HTTP method: GET, POST, PUT, PATCH, DELETE
path yes URL path, supports Fastify params (:id)
query yes GraphQL query or mutation string
vars yes (req) => object — builds GraphQL variables from the request
transform yes (data) => object — shapes the GraphQL response into legacy REST format
schema no Fastify JSON schema for params, querystring, or body — enables coercion and validation
notFound no (data) => boolean — return true to send a 404 response

Environment variables

Variable Default Description
PORT 3000 Port the server listens on
HOST 0.0.0.0 Host the server binds to
GRAPHQL_ENDPOINT http://localhost:5000/graphql GraphQL endpoint to proxy to
API_PREFIX /api URL prefix for all routes
ROUTES_DIR /app/routes Path to route files inside the container
LOG_LEVEL info Fastify log level

Developing restql

Requirements

  • Bun
  • Docker

Setup

git clone <this repo>
cd restql
make install

Running locally

make dev        # run with file watching
make typecheck  # type check

Running via Docker

make up         # start detached
make logs       # tail logs
make restart    # rebuild image and restart
make down       # stop
make shell      # shell into container

Place your own routes/ and mappings/ in the project root — they are gitignored and bind-mounted into the container via compose.override.yml.

Project structure

restql/
├── src/
│   ├── server.ts           # Entry point
│   ├── index.ts            # Public package exports
│   └── lib/
│       └── defineRoutes.ts # Core framework
├── routes/                 # Dev routes (gitignored)
├── mappings/               # Dev mappings (gitignored)
├── Dockerfile
├── compose.yml
├── compose.override.yml
└── Makefile

Make commands

make help       Show all commands
make up         Start via Docker (detached)
make down       Stop containers
make build      Rebuild Docker image
make restart    Rebuild and restart
make logs       Tail gateway logs
make shell      Shell into running container
make install    Install dependencies locally
make dev        Run locally with file watching
make typecheck  TypeScript type check

About

Rest to GraphQL API gateway.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors