In chapter 5 we put a GraphQL endpoint in front of our services and noted, in passing, that the exact technology stack of the API layer isn't important. This bonus chapter proves that claim: we rebuild the same gateway as a REST/OpenAPI service — without touching a single line of the back-end fleet. In the next bonus chapter we'll re-point the web application at it, too.
The finished service is on GitHub: api-rest.
One fleet, any front door
Recall the architecture: the services communicate over a Redis message queue using @imqueue RPC, and the API service is just another consumer of that fleet — it holds a set of typed clients and orchestrates calls to them. What the gateway speaks to the outside world is entirely its own business. GraphQL, REST, gRPC, WebSockets — the fleet neither knows nor cares.
So the plan is simple: keep the orchestration, replace the transport.
What stays the same
Everything that touches @imqueue, literally. The four services — user, auth,
car and time-table — run unmodified. The gateway uses the same statically
generated clients (built with imq client generate, exactly as in
chapter 5), and its start-up
context is the same code you've already seen:
private static async bootstrapContext(): Promise<void> {
Application.context = {
user: new user.UserClient(clientOptions),
auth: new auth.AuthClient(clientOptions),
car: new car.CarClient(clientOptions),
timeTable: new timeTable.TimeTableClient(clientOptions),
};
await Application.context.user.start();
await Application.context.auth.start();
await Application.context.car.start();
await Application.context.timeTable.start();
}
This is the point of the whole exercise: the @imqueue integration is a handful of client instantiations, so it survives an API-style change untouched.
What changes
The transport layer. Instead of graphql-yoga serving a schema, the
gateway is a plain Express 5 application: helmet, cors, compression and
a JSON body parser, followed by an authentication middleware, a REST router
and a terminal error handler.
Where GraphQL gave us an introspectable schema and the GraphiQL playground,
REST has its own well-established equivalents: the gateway builds an
OpenAPI 3 document describing every route and schema (see
src/openapi.ts),
serves it at /openapi.json, and renders an interactive Swagger UI at the
service root — the REST analogue of GraphiQL.
The resulting surface looks like this:
| Method | Path | Description |
|---|---|---|
| POST | /auth/login |
Log in, returns { token, user } |
| POST | /auth/logout |
Invalidate a token |
| GET | /users/me |
Current user (with cars) |
| GET | /users |
List users (non-admins capped at 100) |
| GET | /users/{idOrEmail} |
Get a user by id or email |
| POST | /users |
Register (create) a user |
| PATCH | /users/{id} |
Update the authenticated user |
| POST | /users/{idOrEmail}/cars |
Attach a car (me for self) |
| DELETE | /users/{idOrEmail}/cars/{carId} |
Remove a car |
| GET | /brands |
Car manufacturer names |
| GET | /cars?brand= |
Catalog cars for a brand |
| GET | /cars/{id} |
Catalog car by id |
| GET | /options |
Time-table options |
| GET | /reservations?date= |
Reservations for a date |
| GET | /reservations/{id} |
Reservation by id |
| POST | /reservations |
Make a reservation |
| DELETE | /reservations/{id} |
Cancel a reservation |
From resolvers to an orchestrator
In the GraphQL gateway the orchestration logic lived inside resolvers and
mutations. Here it is collected into a single
Orchestrator
class of transport-agnostic methods that talk to the fleet and return plain
DTOs, while the
routes
stay one-liners:
router.get(
'/reservations',
asyncHandler(async (req, res) => {
const date =
typeof req.query.date === 'string' ? req.query.date : undefined;
res.json({
reservations: await Orchestrator.reservations(ctx(res), date),
});
}),
);
And the orchestrator method behind it is the same @imqueue client call you would have written in a resolver:
public static async reservations(
context: Context,
date?: string,
): Promise<ReservationDTO[]> {
const list = await context.timeTable.list(
date ? new Date(date).toISOString() : undefined,
[...RESERVATION_FIELDS],
);
return Promise.all(
(list as any[]).map(r => Orchestrator.toReservationDTO(context, r)),
);
}
One interesting difference hides in those field lists. In GraphQL, each query
declared exactly which fields it wanted, and the gateway forwarded that
selection to the services. REST has no per-request field selection, so the
gateway pins a fixed field set per DTO (USER_FIELDS, CAR_FIELDS,
RESERVATION_FIELDS) — and still passes it down through the @imqueue clients,
so the services keep returning only what the gateway actually needs.
Authentication
The GraphQL gateway resolved the authenticated user from the X-Auth-User
header; the REST gateway keeps the exact same contract, moved into an Express
middleware that verifies the JWT via the auth service and attaches a
per-request context:
export function authMiddleware(base: Context) {
return async (req: Request, res: Response, next: NextFunction) => {
const token = (req.header('x-auth-user') || '').trim();
const authUser = await resolveAuthUser(base.auth, token);
res.locals.context = { ...base, authUser } as Context;
next();
};
}
The authorization rules — active user, owner-or-admin, admin-only flags — are ported one-to-one from the GraphQL validators into small assertion helpers the orchestrator calls explicitly.
Errors
Failures are emitted in a GraphQL-compatible envelope:
{
"errors": [
{ "message": "Unauthorized", "extensions": { "code": "AUTH_ERROR" } }
]
}
…with a proper HTTP status on top (401 for auth failures, 404 for missing resources, 409 for a duplicate e-mail, 400 otherwise). Keeping the error shape identical across both gateways is a deliberate choice: a single front-end error-mapping routine works against either one — which pays off in the next chapter.
Running it
With the fleet running as described in chapter 6, add the REST gateway alongside:
cd ~/imqueue-sandbox
git clone [email protected]:imqueue-sandbox/api-rest.git
cd api-rest
npm i
npm run dev
The gateway listens on API_REST_PORT (default 8080) and connects to the
fleet over the Redis queue configured by IMQ_REDIS. Open
http://localhost:8080/ for the Swagger UI and try
the endpoints interactively — log in via POST /auth/login, then paste the
returned token into the X-Auth-User authorization field.
Note that nothing stops you from running both gateways at once: GraphQL on port 8888 and REST on port 8080, orchestrating the same services over the same queue, at the same time. That's the adaptability we set out to demonstrate.
Next up: Bonus: REST Web App.