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, andTRACE. 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
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 throws a
METHOD_NOT_ALLOWEDerror. - Ensures the route pattern is valid; otherwise throws a
BAD_REQUESTerror. - Ensures a valid route handler is provided; otherwise throws a
BAD_REQUESTerror.
- Ensures the HTTP method is valid; otherwise throws a
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:
HTTPMethodRoutePatternEndpointConfigRouteHandlerGlobalContext
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
}| 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 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 = HTTPMethodRoute 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 = RoutePatternroute 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"]
}| Option | Type | Description |
|---|---|---|
request | Request | Original Request. |
url | URL | URL object of the incoming request. |
method | HTTPMethod | The HTTP method defined in the endpoint. |
route | RoutePattern | Complete route defined in the endpoint. |
context | GlobalContext | Global context set on the context object in the router. For type safety the module should be augmented. |
headers | Headers | Headers from the incoming request. |
params | ContextParams<Config<["schemas"]>> | Dynamic route parameters inferred from the path or Zod definitions. |
body | ContextBody<Config["schemas"]>["body"] | Parsed and validated body based on the configured schema. |
searchParams | ContextSearchParams<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 = Requestimport { 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 = URLimport { 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 {}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 = HeadersYou 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 }>[]
}>| 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 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
)