Aura Router

createEndpoint

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

createEndpoint does not execute any real logic. It only 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.


What you'll learn

Through this api reference documentation you are going to learn and understand from basic to advanced about the createEndpoint API Reference:


Good to know

  • 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 throws a METHOD_NOT_ALLOWED error.
    • Ensures the route pattern is valid; otherwise throws a BAD_REQUEST error.
    • Ensures a valid route handler is provided; otherwise throws a BAD_REQUEST error.

Type Inference

createEndpoint provides full automatic type inference for all arguments and configuration options, no generics needed. You may still import explicit types if you prefer stricter definitions.

The types can be accessed from the main entry points / or /types. Available types:

  • HTTPMethod
  • RoutePattern
  • EndpointConfig
  • RouteHandler
  • GlobalContext

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

createEndpoint accepts a clear and 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,
  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 HTTPMethod

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

Route RoutePattern

route is a required value in the endpoint definition. It defines the path pattern used for route matching, and it 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"

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

Handler RouteHandler

handler is a required property in every endpoint definition. It defines the logic that runs 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. You never need to manually specify generics unless you explicitly want to.

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 if you want to manually annotate 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 RequestContext

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

To get the most out of 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, Headers } from "@aura-stack/router/types"

interface RequestContext<RouteParams = Record<string, string>, Config extends EndpointConfig = EndpointConfig> {
  headers: Headers
  params: ContextParams<Config["schemas"], RouteParams>["params"]
  body: ContextBody<Config["schemas"]>["body"]
  searchParams: ContextSearchParams<Config["schemas"]>["searchParams"]
}
OptionTypeDescription
requestRequestOriginal Request.
urlURLURL object of the incoming request.
methodHTTPMethodThe HTTP method defined in the endpoint.
routeRoutePatternComplete route defined in the endpoint.
contextGlobalContextGlobal context set on the context object in the router. For type safety the module should be augmented.
headersHeadersHeaders from the incoming request.
paramsContextParams<Config<["schemas"]>>Dynamic route parameters inferred from the path or Zod definitions.
bodyContextBody<Config["schemas"]>["body"]Parsed and validated body based on the configured schema.
searchParamsContextSearchParams<Config["schemas"]>["searchParams"]Parsed and validated query parameters from the request URL.
Request ctx.request

request is present in the RequestContext object. It contains the original, unmodified Request object from the incoming HTTP request.

type RequestHandler = 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 })
})
URL (ctx.url)
type URLContext = URL
import { createEndpoint } from "@aura-stack/router"

const signIn = createEndpoint("GET", "/auth/signIn/:oauth", async (ctx) => {
  const pathname = ctx.url.pathname
  return Response.json({ pathname })
})
Method (ctx.method)
type HTTPMethod = "GET" | "POST" | "DELETE" | "PUT" | "PATCH" | "OPTIONS" | "HEAD" | "TRACE" | "CONNECT"
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" })
})
Route (ctx.route)
type Route = `/${string}`
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" })
})
Context (ctx.context)
interface GlobalContext {}
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 })
})
Headers (ctx.headers)
type ContextHeaders = Headers

You can access and mutate headers using the standard Headers interface:

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

const protectedEndpoint = createEndpoint("GET", "/protected", async (ctx) => {
  const authHeader = ctx.headers.get("authorization")
  ctx.headers.set("x-processed-by", "api")

  return Response.json({ authenticated: !!authHeader }, { headers: ctx.headers })
})
Params Context (ctx.params)
import { z, type ZodObject } from "zod"
import type { EndpointConfig } from "@aura-stack/router/types"

type ContextParams<Schemas extends EndpointConfig["schemas"], Default = Record<string, string>> = Schemas extends {
  params: ZodObject
}
  ? { params: z.infer<Schemas["params"]> }
  : { params: Default }

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
)
Search Parameters (ctx.searchParams)
import { z, type ZodObject } from "zod"
import type { EndpointConfig } from "@aura-stack/router/types"

type ContextSearchParams<Schemas extends EndpointConfig["schemas"]> = Schemas extends { searchParams: ZodObject }
  ? { searchParams: z.infer<Schemas["searchParams"]> }
  : { searchParams: URLSearchParams }

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
)
Body Context (ctx.body)
import { z, type ZodObject } from "zod"
import type { EndpointConfig } from "@aura-stack/router/types"

type ContextBody<Schemas extends EndpointConfig["schemas"]> = Schemas extends { body: ZodObject }
  ? { body: z.infer<Schemas["body"]> }
  : { body: undefined }

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

Without schema

Type: 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
)

Config EndpointConfig

The config argument allows you to attach 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
  middlewares?: 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 params, body and searchParams
Route Params RoutePattern

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">
Schemas EndpointSchemas

schemas is an optional configuration object that allows you to define Zod schemas to validate and type:

For more detailed information about the endpont schemas see the:

  • Route parameters: #params-context
  • Query string parameters: #search-params-context
  • Request body: #body-context
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 middlewares

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.get("x-user-id")
    return Response.json({ userId })
  },
  {
    middlewares: [
      // Authentication middleware
      async (ctx) => {
        const token = ctx.request.headers.get("authorization")

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

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

With multiple middlewares

You can chain several middlewares to compose complex request logic:

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

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

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

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() }),
  },
  middlewares: [
    /* ... */
  ],
})

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

Overload 2: Route + Config

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

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

const config = createEndpointConfig("/users/:userId", {
  middlewares: [
    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
)