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, andTRACE. 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
createEndpointprovides 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
createEndpointConfigfunction. createEndpointvalidates all arguments at definition time:- Ensures the HTTP method is valid; otherwise, it throws a
METHOD_NOT_ALLOWEDerror. - Ensures the route pattern is valid; otherwise, it throws a
BAD_REQUESTerror. - Ensures a valid route handler is provided; otherwise, it throws a
BAD_REQUESTerror.
- Ensures the HTTP method is valid; otherwise, it throws a
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
}| Parameter | Type | Description |
|---|---|---|
method | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "CONNECT" | "OPTIONS" | "TRACE" | The HTTP method used by the endpoint. |
route | RoutePattern | The route definition, supporting static and dynamic segments (e.g., /users/:id). |
handler | RouteHandler | Async function that processes the request and returns a Response |
config | EndpointConfig | Optional 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 = RoutePatternroute 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
}| Option | Type | Description |
|---|---|---|
request | Request | Original Request object. |
url | URL | URL object of the incoming request. |
method | HTTPMethod | The HTTP method defined in the endpoint. |
route | RoutePattern | The complete route pattern defined in the endpoint. |
context | GlobalContext | Global context available via the router configuration. |
headers | HeadersBuilder | A utility for managing response headers and cookies. |
params | ContextParams<Config<["schemas"]>> | Dynamic route parameters inferred from path or Zod definitions. |
body | ContextBody<Config["schemas"]>["body"] | Parsed and validated body based on schema. |
searchParams | ContextSearchParams<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.
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:
| Method | Description |
|---|---|
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 }>[]
}>| Arguments | Type | Description |
|---|---|---|
RouteParams | RoutePattern | The route passed as the second argument to createEndpoint. Determines the dynamic parameters available in the route. |
Schemas | EndpointSchemas | Collection 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:
- 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 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
)