Aura Router

createEndpoint

The createEndpoint function is a declarative API for defining type-safe, framework-agnostic endpoints with automatic parameter inference, optional schema validation, and built-in middleware support. It serves as the primary building block for defining routes in an application.

createEndpoint does not execute any logic definition time. It defines endpoint metadata and provides strong type inference. At runtime, createRouter uses these definitions to perform route matching and execute handlers.

import { createEndpoint } from "@aura-stack/router"

const getSession = createEndpoint("GET", "/auth/session", async (ctx) => {
  return Response.json({
    session: {
      userId: "uuid-123",
      username: "john_doe",
    },
  })
})

All standard HTTP methods are supported: GET, POST, PUT, PATCH, DELETE, HEAD, CONNECT, OPTIONS, and TRACE. See the RFC specification for details.


Overview

This documentation provides a comprehensive guide to the createEndpoint API, covering everything from basic usage to advanced type inference and request context.


Key Concepts

  • createEndpoint provides full type inference, including parameters, search params, request body, middleware output, and more—all derived from the endpoint definition.
  • Configuration options can be passed directly or through the declarative createEndpointConfig function.
  • createEndpoint validates all arguments at definition time:
    • Ensures the HTTP method is valid; otherwise, it throws a METHOD_NOT_ALLOWED error.
    • Ensures the route pattern is valid; otherwise, it throws a BAD_REQUEST error.
    • Ensures a valid route handler is provided; otherwise, it throws a BAD_REQUEST error.

Type Inference

createEndpoint provides full automatic type inference for all arguments and configuration options, eliminating the need for manual generics. Explicit types can still be imported if stricter definitions are preferred.

Types can be accessed from the main entry points / or /types.

Example: Inferred Handler Types

import { z } from "zod"
import { createEndpoint } from "@aura-stack/router"

const credentials = createEndpoint(
  "POST",
  "/auth/credentials",
  async (ctx) => {
    // Body types are inferred from the Zod schema
    const { username, password } = ctx.body
    return Response.json({ token: "jwt-token" })
  },
  {
    schemas: {
      body: z.object({
        username: z.string(),
        password: z.string(),
      }),
    },
  }
)

const signIn = createEndpoint(
  "GET",
  "/auth/signIn/:oauth",
  async (ctx) => {
    // Dynamic params are fully typed
    const { oauth } = ctx.params

    // Search params inferred from schema
    const { redirect_uri } = ctx.searchParams
    return Response.json({ oauth })
  },
  {
    schemas: {
      searchParams: z.object({
        redirect_uri: z.string(),
      }),
    },
  }
)

Example: Using createEndpointConfig Explicitly

import { z } from "zod"
import { createEndpoint, createEndpointConfig } from "@aura-stack/router"

const signInConfig = createEndpointConfig({
  schemas: {
    searchParams: z.object({
      redirect_uri: z.string(),
    }),
  },
})

const credentialsConfig = createEndpointConfig({
  schemas: {
    body: z.object({
      username: z.string(),
      password: z.string(),
    }),
  },
})

const credentials = createEndpoint(
  "POST",
  "/auth/credentials",
  async (ctx) => {
    const { username, password } = ctx.body
    return Response.json({ token: "jwt-token" })
  },
  credentialsConfig
)

const signIn = createEndpoint(
  "GET",
  "/auth/signIn/:oauth",
  async (ctx) => {
    const { oauth } = ctx.params
    const { redirect_uri } = ctx.searchParams
    return Response.json({ oauth })
  },
  signInConfig
)

API Reference

Parameters

The createEndpoint function accepts a structured set of parameters that define an endpoint’s method, route pattern, runtime handler, and configuration options.

import type { HTTPMethod, RoutePattern, EndpointConfig, RouteHandler } from "@aura-stack/router/types"

interface RouteEndpoint<
  Method extends HTTPMethod | HTTPMethod[] = HTTPMethod | HTTPMethod[],
  Route extends RoutePattern = RoutePattern,
  Config extends EndpointConfig = EndpointConfig,
> {
  method: Method
  route: Route
  handler: RouteHandler<Route, Config>
  config: Config
}
ParameterTypeDescription
method"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "CONNECT" | "OPTIONS" | "TRACE"The HTTP method used by the endpoint.
routeRoutePatternThe route definition, supporting static and dynamic segments (e.g., /users/:id).
handlerRouteHandlerAsync function that processes the request and returns a Response
configEndpointConfigOptional configuration for schemas and middlewares

method

The method is a required value in an endpoint definition. It specifies the HTTP method the endpoint responds to. All supported methods follow the HTTP/1.1 RFC specification.

import type { HTTPMethod } from "@aura-stack/router/types"

// Expected: "GET" | "POST" | "DELETE" | "PUT" | "PATCH" | "OPTIONS" | "HEAD" | "TRACE" | "CONNECT"
export type Methods = HTTPMethod | HTTPMethod[]

route

The route is a required value in the endpoint definition. It defines the path pattern used for route matching and must always start with a leading slash.

import type { RoutePattern } from "@aura-stack/router/types"

// Expected: `/${string}`
export type Route = RoutePattern
The route value must begin with a leading slash, e.g., /users, /users/:id, /auth/signIn/.
import type { RoutePattern } from "@aura-stack/router/types"

export const route: RoutePattern = "/auth/signIn/:oauth"

handler

The handler is a required property in every endpoint definition. It defines the logic that executes when an incoming request matches the endpoint’s route.

import type { Prettify, RoutePattern, EndpointConfig, GetRouteParams } from "@aura-stack/router/types"

type RouteHandler<Route extends RoutePattern, Config extends EndpointConfig> = (
  request: Request,
  ctx: Prettify<RequestContext<GetRouteParams<Route>, Config>>
) => Response | Promise<Response>

createEndpoint automatically infers all generic types based on the provided route pattern and configuration. Generics do not need to be manually specified unless explicitly required.

import { z } from "zod"
import { createEndpoint } from "@aura-stack/router"

export const endpoint = createEndpoint(
  "POST",
  "/auth/credentials",
  async (ctx) => {
    const { body } = ctx.body
    return Response.json({ body })
  },
  {
    schemas: {
      body: z.object({
        username: z.string(),
        password: z.string(),
      }),
    },
  }
)
Explicit Context Definition

The RouteHandler type can also be used explicitly. This is useful for manually annotating the expected params, body, or searchParams for advanced typing scenarios.

import type { RouteHandler } from "@aura-stack/router/types"

const endpoint: RouteHandler<
  "/auth/:oauth",
  {
    schemas: {
      searchParams: ZodObject<{ state: ZodString }>
    }
  }
> = (ctx) => {
  const { oauth } = ctx.params
  const { state } = ctx.searchParams
  return Response.json({ message: `OAuth: ${oauth}, State: ${state}` })
}
Request Context

Every route handler receives a RequestContext object as the argument. This context provides parsed, validated, and fully typed information derived from the request.

To maximize type inference, define route parameters in the route path and provide Zod schemas in the endpoint config to validate and infer types for params, body, and searchParams.

import type { EndpointConfig, ContextParams, ContextBody, ContextSearchParams, HeadersBuilder } from "@aura-stack/router/types"

interface RequestContext<RouteParams = Record<string, string>, Config extends EndpointConfig = EndpointConfig> {
  headers: HeadersBuilder
  params: ContextParams<Config["schemas"], RouteParams>["params"]
  body: ContextBody<Config["schemas"]>["body"]
  searchParams: ContextSearchParams<Config["schemas"]>["searchParams"]
  request: Request
  url: URL
  method: HTTPMethod
  route: RoutePattern
  context: GlobalContext
}
OptionTypeDescription
requestRequestOriginal Request object.
urlURLURL object of the incoming request.
methodHTTPMethodThe HTTP method defined in the endpoint.
routeRoutePatternThe complete route pattern defined in the endpoint.
contextGlobalContextGlobal context available via the router configuration.
headersHeadersBuilderA utility for managing response headers and cookies.
paramsContextParams<Config<["schemas"]>>Dynamic route parameters inferred from path or Zod definitions.
bodyContextBody<Config["schemas"]>["body"]Parsed and validated body based on schema.
searchParamsContextSearchParams<Config["schemas"]>["searchParams"]Parsed and validated query parameters.
ctx.request

The request property contains the original, unmodified Request object from the incoming HTTP request.

import { createEndpoint } from "@aura-stack/router"

const signIn = createEndpoint("GET", "/auth/signIn/:oauth", async (ctx) => {
  const isCached = ctx.request.cache
  return Response.json({ cache: isCached })
})
ctx.url

The url property provides the parsed URL object of the request.

import { createEndpoint } from "@aura-stack/router"

const signIn = createEndpoint("GET", "/auth/signIn/:oauth", async (ctx) => {
  const pathname = ctx.url.pathname
  return Response.json({ pathname })
})
ctx.method

The method property indicates the HTTP method used for the request.

import { createEndpoint } from "@aura-stack/router"

const supportedBodyMethods = new Set<HTTPMethod>(["POST", "PUT", "PATCH"])

const signIn = createEndpoint("POST", "/auth/signIn", async (ctx) => {
  if (!supportedBodyMethods.has(ctx.method)) {
    return Response.json({ message: "Unauthorized" }, { status: 401 })
  }
  return Response.json({ message: "Authenticated" })
})
ctx.route

The route property holds the route pattern string defined for the endpoint.

import { createEndpoint } from "@aura-stack/router"

const deprecatedRoutes = new Set<string>(["/api/v1/users", "api/v1/books"])

const getUsers = createEndpoint("GET", "/api/v1/users", async (ctx) => {
  if (deprecatedRoutes(ctx.route)) {
    return Response.redirect("/api/v2/services/users")
  }
  return Response.json({ message: "Successful" })
})
ctx.context

The context property provides access to the global context managed by the router.

types.d.ts
import type { GlobalContext } from "@aura-stack/router"

declare module "@aura-stack/router" {
  interface GlobalContext {
    db: DatabaseInstance
  }
}
import { createEndpoint } from "@aura-stack/router"

const getUser = createEndpoint("GET", "/users/:userId", async (ctx) => {
  const { userId } = ctx.params
  const { db } = ctx.context

  const user = await db.query("SELECT * FROM users WHERE id = ?", [userId])
  return Response.json({ user })
})
ctx.headers

The headers property is an instance of HeadersBuilder, which simplifies managing response headers and cookies. It allows setting headers, reading/setting cookies, and retrieving header values.

Methods:

MethodDescription
setHeader(name, value)Sets a header value.
getHeader(name)Retrieves a header value.
setCookie(name, value, options)Sets a cookie with optional serialization options.
getCookie(name)Retrieves a specific cookie by name.
getSetCookie(name)Retrieves a specific Set-Cookie header value.
import { createEndpoint } from "@aura-stack/router"

const protectedEndpoint = createEndpoint("GET", "/protected", async (ctx) => {
  const authHeader = ctx.headers.getHeader("authorization")

  ctx.headers.setHeader("x-processed-by", "api")
  ctx.headers.setCookie("session_id", "xyz123", { httpOnly: true, secure: true })

  return Response.json({ authenticated: !!authHeader }, { headers: ctx.headers.toHeaders() })
})
ctx.params

Dynamic segments from the route pattern are automatically extracted and typed.

import { createEndpoint } from "@aura-stack/router"

/* 
  Single parameter 
*/
const getUser = createEndpoint("GET", "/users/:userId", async (ctx) => {
  const { userId } = ctx.params
  return Response.json({ id: userId })
})

/* 
  Multiple parameters
*/
const getBookmark = createEndpoint("GET", "/users/:userId/books/:bookId", async (ctx) => {
  // Both are typed as string
  const { userId, bookId } = ctx.params
  return Response.json({ userId, bookId })
})

With Zod schema (returns a fully typed object):

import { z } from "zod"
import { createEndpoint, createEndpointConfig } from "@aura-stack/router"

const config = createEndpointConfig("/users/:userId", {
  schemas: {
    params: z.object({
      userId: z.coerce.number(),
    }),
  },
})

const getUser = createEndpoint(
  "GET",
  "/users/:userId",
  async (ctx) => {
    const { userId } = ctx.params
    return Response.json({ id: userId })
  },
  config
)
ctx.searchParams

Query string parameters are accessible through ctx.searchParams.

Without schema: Returns URLSearchParams (native browser API).

import { createEndpoint } from "@aura-stack/router"

const searchUsers = createEndpoint("GET", "/users/search", async (ctx) => {
  const query = ctx.searchParams.get("q")
  const page = ctx.searchParams.get("page")

  return Response.json({ query, page })
})

With Zod schema: Returns a fully typed object.

import { z } from "zod"
import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

const searchConfig = createEndpointConfig({
  schemas: {
    searchParams: z.object({
      q: z.string().min(1),
      page: z.coerce.number().int().positive().default(1),
    }),
  },
})

const searchUsers = createEndpoint(
  "GET",
  "/users/search",
  async (ctx) => {
    const { q, page } = ctx.searchParams
    return Response.json({ query: q, page })
  },
  searchConfig
)
ctx.body

For POST, PUT, and PATCH requests, the body is parsed and available in the context.

Without schema: Type is unknown.

import { createEndpoint } from "@aura-stack/router"

const createPost = createEndpoint("POST", "/posts", async (ctx) => {
  const body = ctx.body
  return Response.json({ data: body })
})

With Zod schema: The returned value is fully typed.

import { createEndpointConfig, createEndpoint } from "@aura-stack/router"
import { z } from "zod"

const createPostConfig = createEndpointConfig({
  schemas: {
    body: z.object({
      title: z.string().min(1),
      content: z.string(),
      tags: z.array(z.string()).optional(),
    }),
  },
})

const createPost = createEndpoint(
  "POST",
  "/posts",
  async (ctx) => {
    const { title, content, tags } = ctx.body

    return Response.json({ id: "new-post", title, content, tags }, { status: 201 })
  },
  createPostConfig
)

Endpoint Configuration

The config argument allows defining optional Zod schemas and middlewares to an endpoint.

import type { RoutePattern, EndpointSchemas, Prettify, MiddlewareFunction, GetRouteParams } from "@aura-stack/router/types"

type EndpointConfig<
  RouteParams extends RoutePattern = RoutePattern,
  Schemas extends EndpointSchemas = EndpointSchemas,
> = Prettify<{
  schemas?: Schemas
  use?: MiddlewareFunction<GetRouteParams<RouteParams>, { schemas: Schemas }>[]
}>
ArgumentsTypeDescription
RouteParamsRoutePatternThe route passed as the second argument to createEndpoint. Determines the dynamic parameters available in the route.
SchemasEndpointSchemasCollection of Zod schemas for validating and typing parameters.
Route Params

The route defines the endpoint path and determines the names of the dynamic parameters. These parameters are extracted and typed automatically.

import type { GetRouteParams } from "@aura-stack/router/types"

// Expected: { userId: string, bookId: string }
type RouteParams = GetRouteParams<"/users/:userId/books/:bookId">
Zod Schemas

The schemas object allows defining Zod schemas to validate and type:

import type { ZodObject } from "zod"

interface EndpointSchemas {
  body?: ZodObject<any>
  searchParams?: ZodObject<any>
  params?: ZodObject<any>
}

Usage

The fourth parameter of createEndpoint accepts an optional configuration object that enables schema validation and middleware execution. For improved type inference, it is recommended to use createEndpointConfig.

Basic Usage

import { createEndpoint, createEndpointConfig } from "@aura-stack/router"

export const signIn = createEndpoint("GET", "/auth/signIn/:oauth", async (ctx) => {
  const { oauth } = ctx.params
  return Response.redirect("https://service.com/authorization", { status: 302 })
})

With use

Middlewares run before the route handler and have access to the parsed and typed context (ctx).

import { createEndpoint } from "@aura-stack/router"

const getProfile = createEndpoint(
  "GET",
  "/profile",
  async (ctx) => {
    const userId = ctx.headers.getHeader("x-user-id")
    return Response.json({ userId })
  },
  {
    use: [
      // Authentication middleware
      async (ctx) => {
        const token = ctx.request.headers.get("authorization")

        if (!token) {
          throw new Error("Unauthorized")
        }

        // Attach user information
        ctx.headers.setHeader("x-user-id", "user-123")
        return ctx
      },
    ],
  }
)

With multiple middlewares

Several middlewares can be chained to compose complex request logic:

const config = createEndpointConfig({
  use: [
    // Logging
    async (ctx) => {
      console.log(`Request to ${ctx.url}`)
      return ctx
    },

    // Rate limiting
    async (ctx) => {
      // Rate limit logic
      ctx.headers.setHeader("x-rate-limit", "100")
      return ctx
    },

    // Authentication
    async (ctx) => {
      const token = ctx.headers.getHeader("authorization")
      if (!token) throw new Error("Unauthorized")
      return ctx
    },
  ],
})

With multiple methods

createEndpoint also supports defining multiple HTTP methods for the same route pattern. This allows handling different methods with the same logic or configuration.

const multiMethodEndpoint = createEndpoint(["GET", "POST"], "/multi", async (ctx) => {
  if (ctx.method === "GET") {
    return Response.json({ message: "Handled GET request" })
  } else if (ctx.method === "POST") {
    return Response.json({ message: "Handled POST request" })
  }
})

With createEndpointConfig

The createEndpointConfig helper provides two overloads to enhance type inference.

Overload 1: Configuration Only

When only a configuration object is provided, route parameters default to an empty object:

const config = createEndpointConfig({
  schemas: {
    body: z.object({ name: z.string() }),
  },
  use: [
    /* ... */
  ],
})

// Use with any endpoint
const endpoint = createEndpoint("POST", "/users", handler, config)

Overload 2: Route + Config

When the route pattern is passed as the first argument to createEndpointConfig, the dynamic route parameters become fully typed inside middlewares. This provides better type inference and avoids manually defining generics.

import { createEndpointConfig, createEndpoint } from "@aura-stack/router"

const config = createEndpointConfig("/users/:userId", {
  use: [
    async (ctx) => {
      const { userId } = ctx.params
      console.log(`Processing request for user ${userId}`)
      return ctx
    },
  ],
})

const endpoint = createEndpoint(
  "GET",
  "/users/:userId",
  async (ctx) => {
    const { userId } = ctx.params
    return Response.json({ id: userId })
  },
  config
)

On this page