Endpoint
An Endpoint is a special Component that handles HTTP requests and returns a Response object.
They come in two flavors:
Endpointfor user-facing endpointsAdminEndpointfor admin-only endpoints
Definition
import { Endpoint, type HttpContext } from "@tymber/core";
export class MyEndpoint extends Endpoint {
async handle(ctx: HttpContext) {
return Response.json({
hello: "world",
});
}
}
Registration
import { type Module, type AppInit } from "@tymber/core";
import { MyEndpoint } from "./endpoints/MyEndpoint";
export const MyModule: Module = {
name: "my-module",
version: "1.2.3",
init(app: AppInit) {
app.endpoint("GET", "/hello", MyEndpoint);
// with path parameters
app.endpoint("GET", "/orders/:orderId/items/:itemId", ReadOrderItem);
// admin endpoint
app.adminEndpoint("GET", "/hello", MyAdminEndpoint);
},
};
HTTP context
The HttpContext object is a special Context object that contains information about the current HTTP request:
export interface HttpContext<Payload = any, PathParams = any, QueryParams = any>
extends Context {
method: HttpMethod;
payload: Payload;
path: string;
pathParams: PathParams;
query: QueryParams;
headers: Headers;
cookies: Record<string, string>;
abortSignal: AbortSignal;
locale: Locale;
responseHeaders: Headers;
sessionId?: string;
adminSessionId?: string;
render: (
view: string | string[],
data?: Record<string, any>,
) => Promise<Response>;
redirect(path: string, type?: HttpRedirectCode): Response;
}
Validation
Tymber validates HTTP requests before they reach your handle() method with AJV.
If validation fails, it returns a HTTP 400 Bad Request response with error details.
Request body
To validate the request body, define a payloadSchema property using JSON Schema:
import { Endpoint, type HttpContext } from "@tymber/core";
import type { JSONSchemaType } from "ajv";
interface Payload {
title: string;
description?: string;
dueDate?: string;
};
export class CreateTodo extends Endpoint {
payloadSchema: JSONSchemaType<Payload> = {
type: "object",
properties: {
title: { type: "string", minLength: 1 },
description: { type: "string", nullable: true },
dueDate: { type: "string", format: "date-time", nullable: true },
},
required: ["title"],
additionalProperties: false,
};
async handle(ctx: HttpContext<CreateTodoPayload>) {
// at this point, ctx.payload is guaranteed to match the Payload interface
const { payload } = ctx;
// persist the entity
return new Response(null, { status: 201 });
}
}
Path parameters
To validate the path parameters (e.g., /todos/:todoId), define a pathParamsSchema property:
import { Endpoint, type HttpContext } from "@tymber/core";
import { type JSONSchemaType } from "ajv";
interface PathParams {
todoId: string;
}
export class ReadTodo extends Endpoint {
pathParamsSchema: JSONSchemaType<PathParams> = {
type: "object",
properties: {
todoId: { type: "string", minLength: 1 },
},
required: ["todoId"],
additionalProperties: false,
};
async handle(ctx: HttpContext<never, PathParams>) {
// at this point, ctx.pathParams is guaranteed to match the PathParams interface
const { pathParams } = ctx;
const { todoId } = pathParams;
// fetch the entity
return Response.json(item);
}
}
Query parameters
To validate the path parameters (e.g., /todos?completed=true), define a querySchema property:
import { Endpoint, type HttpContext } from "@tymber/core";
import { type JSONSchemaType } from "ajv";
interface Query {
completed: boolean;
}
export class ListTodos extends Endpoint {
querySchema: JSONSchemaType<Query> = {
type: "object",
properties: {
completed: { type: "boolean", nullable: true, default: false },
},
required: [],
additionalProperties: false,
};
async handle(ctx: HttpContext<never, never, Query>) {
// at this point, ctx.query is guaranteed to match the Query interface
const { query } = ctx;
// fetch the entities
return Response.json(items);
}
}
Authentication and anonymous access
By default, an anonymous request will return a HTTP 401 Unauthorized response.
You can allow anonymous access by setting allowAnonymous = true in your endpoint:
import { Endpoint, type HttpContext } from "@tymber/core";
export class MyEndpoint extends Endpoint {
allowAnonymous = true;
async handle(ctx: HttpContext) {
return Response.json({
hello: "world",
});
}
}
Authorization
Authorization checks happen in the hasPermission(ctx) method.
An unauthorized request will return a HTTP 403 Forbidden response.
import { Endpoint, GroupId, type HttpContext } from "@tymber/core";
enum Role {
MANAGER = 0,
READER = 1,
}
interface PathParams {
groupId: GroupId;
}
export class ListGroupItems extends Endpoint {
pathParamsSchema: JSONSchemaType<PathParams> = {
type: "object",
properties: {
groupId: { type: "string", minLength: 1 },
},
required: ["groupId"],
additionalProperties: false,
}
hasPermission(ctx: HttpContext) {
const { groupId } = ctx.pathParams;
return ctx.user.groups.some(
(group) => group.id === groupId && group.role === Role.MANAGER,
);
}
async handle(ctx: HttpContext<never, PathParams>) {
// at this point, user is guaranteed to have the MANAGER role for the given group
const { pathParams } = ctx;
const { groupId } = pathParams;
// ...
return Response.json(items);
}
}