Skills Development Webiny Custom HTTP Routes

Webiny Custom HTTP Routes

v20260911
webiny-http-route
Add custom HTTP endpoints (GET, POST, PUT, etc.) to the Webiny API alongside the GraphQL handler using <Api.Route> and HttpRouteHandler, with full dependency injection support.
Get Skill
416 downloads
Overview

Custom HTTP Routes

TL;DR

Write a handler that implements HttpRouteHandler.Interface, then point <Api.Route> at it in
webiny.config.tsx. The method and path props configure both the API Gateway route and the
router, so the handler file never restates them. Handlers get full DI.

YOU MUST include the full file path with the .ts extension in the src prop. Use
src={"/extensions/MyRoute.ts"}, not src={"/extensions/MyRoute"}. Omitting it fails the build.

YOU MUST use export default for the createImplementation() call. Named exports fail here.

The route pattern

// extensions/MyRoute.ts
import { HttpRouteHandler, Logger } from "webiny/api";

class MyRouteImpl implements HttpRouteHandler.Interface {
  constructor(private logger: Logger.Interface) {}

  async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) {
    this.logger.info({ path: request.path }, "Handling request");

    return response.status(200).json({ status: "ok" });
  }
}

export default HttpRouteHandler.createImplementation({
  implementation: MyRouteImpl,
  dependencies: [Logger]
});

Register it:

<Api.Route method={"POST"} path={"/my-route"} src={"/extensions/MyRoute.ts"} />

Props reference

PropTypeRequiredDescription
pathstringYesRoute path — must start with /
methodstringYesHTTP method (see below)
srcstringYesPath to the handler file (must include .ts)
routeNamestringNoRoute name (kebab-case). Derived from path + method if omitted. Doubles as the Pulumi resource name and the id a decorator matches on

Methods: DELETE, GET, HEAD, PATCH, POST, PUT, OPTIONS, ANY. Use ANY to match every
method on a path.

Path parameters

Write them either way. {orderId} is API Gateway syntax, :orderId is the router's; the extension
converts to whichever each consumer needs, so both work and mean the same thing.

<Api.Route method={"GET"} path={"/orders/{orderId}"} src={"/extensions/GetOrderRoute.ts"} />
<Api.Route method={"GET"} path={"/orders/:orderId"} src={"/extensions/GetOrderRoute.ts"} />

Read them off request.pathParameters:

class GetOrderRouteImpl implements HttpRouteHandler.Interface {
  constructor(private getOrder: GetOrderUseCase.Interface) {}

  async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) {
    const order = await this.getOrder.execute(request.pathParameters.orderId);

    return response.status(200).json(order);
  }
}

export default HttpRouteHandler.createImplementation({
  implementation: GetOrderRouteImpl,
  dependencies: [GetOrderUseCase]
});

Wildcards are the exception: /files/* matches for the router, {proxy+} for API Gateway, and they
capture differently. Write a wildcard route for the target you mean.

Request

HttpRouteHandler.Request is transport-agnostic — no API Gateway or Node types leak into your code.

interface Request {
  method: string;
  path: string;
  headers: Record<string, string>;
  query: Record<string, string>;
  pathParameters: Record<string, string>;
  body: any;
  /** Which route matched — `{ name, method, path }`. */
  route: MatchedRouteDefinition;
}

route is what makes a decorator able to act on one route (see below), and lets a handler read its
own identity. method/path on it are the route's PATTERN (/orders/:orderId), where the
top-level method/path are the request's actual values (/orders/abc123).

Response

HttpRouteHandler.Response is a mutable builder, the res of an Express-style handler. Every method
returns this, so calls chain:

MethodPurpose
status(code)Set the status code (defaults to 200)
json(body)JSON body + content type
text(body)Plain-text body
send(body)Body as-is
header(name, value)Set one header
getHeader(name)Read a header already set
cookie(name, value, options?)Set a cookie
clearCookie(name, options?)Expire a cookie
redirect(url, statusCode?)Redirect
sse(source)Server-sent events stream
return response.status(201).cookie("sid", id, { httpOnly: true }).json({ id });

Returning the builder is optional — mutate it and return nothing for the same result. Returning a
plain object works too; anything set on the builder is merged underneath it, and the returned object
wins on conflicts.

cookie's maxAge is in seconds (the Max-Age attribute), unlike Express, which uses
milliseconds.

How it works

<Api.Route> does two things:

  1. Build time — writes a registration into apps/api/graphql/src/extensions.ts that registers an

HttpRouteDefinition built from your method and path, pointing at your handler.

  1. Deploy time — calls addRoute({ name, path, method }) on the API Pulumi module to create the

API Gateway route, with the path converted to {param} syntax.

At request time the router matches the definition (cheap — it holds only method, path and the
handler class) and builds your handler only then, with its dependencies injected. A route your
request didn't match is never constructed.

Decorating a route

Two hooks, both ordinary DI decorators.

Change what a route DOES

Decorate HttpRouteHandler. It applies to every route, and request.route.name picks the one you
mean:

import { HttpRouteHandler } from "webiny/api";

export default HttpRouteHandler.createDecorator({
  decorator: class implements HttpRouteHandler.Interface {
    constructor(private decoratee: HttpRouteHandler.Interface) {}

    async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) {
      if (request.route.name !== "my-route-get") {
        return this.decoratee.handle(request, response);
      }

      if (request.headers["x-api-key"] !== "expected") {
        return response.status(401).json({ message: "Not authorized." });
      }

      return this.decoratee.handle(request, response);
    }
  },
  dependencies: []
});

Drop the name check and it wraps every route, which is what you want for timing or logging.

request.route is { name, method, path } of the matched route. name is routeName, or the
value derived from path and method (/my-route + GET → my-route-get). A route can read it to
find out its own identity too.

Change what a route IS

Decorate HttpRouteDefinition when you need to alter the route itself — point it at a different
handler, or move its path:

import { HttpRouteDefinition } from "webiny/api";

export default HttpRouteDefinition.createDecorator({
  decorator: class implements HttpRouteDefinition.Interface {
    readonly name: string;
    readonly method: string;
    readonly path: string;
    readonly handler: HttpRouteDefinition.Interface["handler"];

    constructor(decoratee: HttpRouteDefinition.Interface) {
      this.name = decoratee.name;
      this.method = decoratee.method;
      this.path = decoratee.name === "my-route-get" ? "/moved" : decoratee.path;
      this.handler = decoratee.handler;
    }
  },
  dependencies: []
});

A decorator exposes the same properties as what it wraps, so it copies through the ones it doesn't
change. Getters work too if you prefer them — these are plain properties, not methods.

Key rules

  • Do not declare method/path in the handler file. They come from the props. A handler that

sets its own would be ignored, and the two could disagree.

  • Export the handler, not a definition. <Api.Route> builds the definition for you.
  • Constructor parameter order must match the dependencies array exactly.
  • Declare dependencies; don't inject the container and resolve inside handle().
  • One handler per file — each src file default-exports one implementation.
  • Use .js extensions in relative imports (ESM).
  • Do not read process.env at runtime; use BuildParams.
  • No console.* in API code — inject Logger.

Quick reference

Import:      import { HttpRouteHandler } from "webiny/api";
Interface:   HttpRouteHandler.Interface
Request:     HttpRouteHandler.Request
Response:    HttpRouteHandler.Response
Export:      export default HttpRouteHandler.createImplementation({ implementation, dependencies })
Register:    <Api.Route method={"POST"} path={"/my-route"} src={"/extensions/MyRoute.ts"} />
Deploy:      yarn webiny deploy api --env=dev

Related skills

  • webiny-api-architect — DI patterns, services, use cases, feature organization
  • webiny-custom-graphql-api — custom GraphQL endpoints (alternative to HTTP)
  • webiny-dependency-injection — injectable services catalog (Logger, BuildParams, etc.)
  • webiny-infrastructure-extensions — Pulumi-level infrastructure customization
Info
Category Development
Name webiny-http-route
Version v20260911
Size 10.14KB
Updated At 2026-09-12
Language