Extending the REST API

The hub server uses Express v5 as its underlying http server.

The hub server is first and foremost a GraphQL server that exposes the POST /graphql REST route.

However, you may want to add additional REST routes. e.g. GET /health or GET /metrics or maybe even a custom bet endpoint if you don’t want to use GraphQL.

  1. Using configureApp
  2. Example
  3. Accessing current user

Using configureApp

To access the underlying Express instance, pass a function to the configureApp option.

const options: ServerOptions = {
  configureApp(app: Express) {
    // Do anything with `app` here, like add routes or middleware
  },
};

Custom routes and middleware inside configureApp are run downstream of @moneypot/hub internal middleware.

Example

import { ServerOptions, startAndListen } from "@moneypot/hub";
import {
  Express,
  CaasRequest,
  Response,
  NextFunction,
} from "@moneypot/hub/express";

const options: ServerOptions = {
  configureApp(app: Express) {
    app.use((req: CaasRequest, res: Response, next: NextFunction) => {
      switch (req.identity?.kind) {
        case "user":
          console.log("Logged in as user", req.identity.user.uname);
          break;
        case "operator":
          console.log("Logged in as operator");
          break;
        default:
          console.log("Unauthenticated request");
          break;
      }
      next();
    });

    app.get("/health", (req: CaasRequest, res: Response) => {
      res.json({ status: "healthy" });
    });
  },
  // ...
};

startAndListen(options).then(({ port }) => {
  console.log(`Listening on port ${port}`);
});

You don’t have to use the Express re-exports from @moneypot/hub/express, but doing so ensures you’re using the same version of Express components that are bundled with hub.

Accessing current user

You can access the user info from the request object: req.identity.

It represents three states:

  1. Request is not authenticated
  2. A user is making the request (Authorization: "session:{sessionToken}" was valid)
  3. An operator’s api key is making the request (Authorization: "apikey:{apiKey}" was valid)

The shape of req.identity is:

export interface CaasRequest extends Request {
  identity?:
    | {
        kind: "user";
        user: DbUser;
        sessionId: string;
      }
    | { kind: "operator"; apiKey: string };
}

So, you could use it like this to restrict route access to logged-in users:

import {
  Express,
  CaasRequest,
  Response,
  NextFunction,
} from "@moneypot/hub/express";
import { DbUser } from "@moneypot/hub/db";
import * as database from "./database";

app.get("/bets", (req: CaasRequest, res: Response, next: NextFunction) => {
  if (req.identity?.kind !== "user") {
    return res.status(401).send("Unauthorized");
  }

  const user: DbUser = req.identity.user;

  const bets = await database.listBetsForUserId(user.id);
  res.json(bets);
});

And here’s how you can use it for a route that is limited to operator api keys:

// Maybe we have a GET /metrics route that only the operator can access
app.get("/metrics", (req: CaasRequest, res: Response, next: NextFunction) => {
  if (req.identity?.kind !== "operator") {
    return res.status(401).send("Unauthorized");
  }

  res.json({ metrics: "TODO" });
});