npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@forjakit/scheduling-fastify

v0.1.1

Published

Fastify plugin that exposes `@forjakit/scheduling` as a REST API. Registers CRUD routes for scheduled events with tenant isolation, input validation, auth guard hooks, and structured error responses.

Readme

@forjakit/scheduling-fastify

Fastify plugin that exposes @forjakit/scheduling as a REST API. Registers CRUD routes for scheduled events with tenant isolation, input validation, auth guard hooks, and structured error responses.

Installation

pnpm add @forjakit/scheduling-fastify

Peer dependencies: @forjakit/scheduling, fastify >= 5, zod >= 3.24.

Plugin Registration

import Fastify from "fastify";
import { schedulingPlugin } from "@forjakit/scheduling-fastify";
import { createSchedulingService } from "@forjakit/scheduling";

const app = Fastify();

const service = createSchedulingService({
  storage: myStorage,
  metadataSchema: myMetadataSchema,
});

app.register(schedulingPlugin, {
  service,
  prefix: "/scheduling",
  tenantResolver: (req) => req.headers["x-tenant-id"] as string,
  guards: {
    write: async (req, reply) => { /* your auth logic */ },
    statusTransition: async (req, reply) => { /* optional, defaults to write */ },
  },
});

SchedulingPluginOptions

| Option | Type | Required | Default | Description | | ----------------- | ----------------------------------------- | -------- | ----------------------------- | ----------------------------------------- | | service | SchedulingService<TMeta> | Yes | -- | Service instance from createSchedulingService | | prefix | string | No | "" | Route prefix (e.g., "/scheduling") | | tenantResolver | (req: FastifyRequest) => string | No | Reads x-tenant-id header | Extracts tenant ID from the request | | guards.write | (req, reply) => Promise<void> | No | None (unguarded) | Pre-handler for create, update, delete | | guards.statusTransition | (req, reply) => Promise<void> | No | Falls back to guards.write | Pre-handler for status transitions |

Routes

All routes are relative to the configured prefix. Tenant ID is resolved from every request via tenantResolver.

| Method | Path | Guard | Description | | -------- | ----------------------- | ------------------ | ---------------------- | | POST | /events | write | Create event | | GET | /events | None | List events | | GET | /events/:id | None | Get event by ID | | PATCH | /events/:id | write | Update event | | DELETE | /events/:id | write | Delete event | | PATCH | /events/:id/status | statusTransition | Transition status | | POST | /events/:id/cancel | statusTransition | Cancel event |

POST /events

Creates a new scheduled event.

Request body:

{
  "contextId": "championship-2026",
  "participants": [
    { "id": "team-a", "label": "Team Alpha", "type": "team" },
    { "id": "team-b", "label": "Team Beta", "type": "team" }
  ],
  "scheduledAt": "2026-04-01T14:00:00Z",
  "durationMinutes": 90,
  "venue": { "id": "court-1", "label": "Main Court", "type": "court" },
  "metadata": { "round": 1 }
}

Note: tenantId is injected by the plugin from tenantResolver, not from the request body.

Response: 201 Created

{
  "event": {
    "id": "clx...",
    "tenantId": "org-1",
    "contextId": "championship-2026",
    "participants": [...],
    "scheduledAt": "2026-04-01T14:00:00.000Z",
    "durationMinutes": 90,
    "venue": { "id": "court-1", "label": "Main Court", "type": "court" },
    "status": "SCHEDULED",
    "metadata": { "round": 1 },
    "createdAt": "...",
    "updatedAt": "..."
  }
}

GET /events

Lists events with optional filters via query parameters.

Query parameters:

| Param | Type | Description | | --------------- | -------- | ---------------------------------------------- | | contextId | string | Filter by context | | participantId | string | Filter by participant | | status | string | Single status or comma-separated (e.g., SCHEDULED,IN_PROGRESS) | | from | string | ISO date string, lower bound for scheduledAt | | to | string | ISO date string, upper bound for scheduledAt |

Response: 200 OK

{
  "events": [...]
}

GET /events/:id

Retrieves a single event.

Response: 200 OK

{
  "event": { ... }
}

PATCH /events/:id

Updates mutable fields of a SCHEDULED or IN_PROGRESS event.

Request body: All fields optional.

{
  "scheduledAt": "2026-04-02T14:00:00Z",
  "durationMinutes": 60,
  "venue": null,
  "metadata": { "round": 2 }
}

Response: 200 OK

{
  "event": { ... }
}

DELETE /events/:id

Permanently deletes a SCHEDULED event.

Response: 204 No Content

PATCH /events/:id/status

Transitions event status following the allowed lifecycle.

Request body:

{
  "status": "IN_PROGRESS"
}

Response: 200 OK

{
  "event": { ... }
}

POST /events/:id/cancel

Convenience endpoint to cancel an event (equivalent to transitioning status to CANCELLED).

Response: 200 OK

{
  "event": { ... }
}

Guards

Guards are Fastify pre-handler hooks injected before route handlers. Use them to enforce authentication and authorization.

app.register(schedulingPlugin, {
  service,
  guards: {
    write: async (req, reply) => {
      const token = req.headers.authorization?.replace("Bearer ", "");
      if (!token) {
        reply.status(401).send({ error: "UNAUTHORIZED" });
        return;
      }
      // Verify token, check permissions...
    },
    statusTransition: async (req, reply) => {
      // More restrictive guard for status changes, if needed
    },
  },
});

Guard assignment:

| Operation | Guard Used | | ------------------ | ----------------------------------------------- | | Create event | guards.write | | Update event | guards.write | | Delete event | guards.write | | Transition status | guards.statusTransition (falls back to guards.write) | | Cancel event | guards.statusTransition (falls back to guards.write) | | List events | None | | Get event | None |

If no guards are provided, all routes are unguarded.

Tenant Resolution

By default, the plugin reads x-tenant-id from the request headers. If the header is missing, it throws a SchedulingError with code TENANT_REQUIRED (400).

Provide a custom resolver to extract the tenant ID from any source:

app.register(schedulingPlugin, {
  service,
  tenantResolver: (req) => {
    // From JWT claims
    return (req as any).user.tenantId;
  },
});

Error Handling

The plugin registers a scoped Fastify error handler that converts known errors into structured JSON responses.

SchedulingError Response

{
  "error": "PARTICIPANT_CONFLICT",
  "message": "Participant(s) team-1 already have an event at 2026-04-01T14:00:00.000Z"
}

HTTP status codes are taken directly from the SchedulingError.statusCode property. See the core package documentation for the full error code table.

ZodError Response (Validation Failures)

{
  "error": "VALIDATION_ERROR",
  "message": "Invalid input",
  "details": [
    { "field": "durationMinutes", "message": "Expected number, received string" },
    { "field": "participants", "message": "Array must contain at least 1 element(s)" }
  ]
}

HTTP status: 400.

Unhandled Errors

Any error that is not a SchedulingError or ZodError is re-thrown to Fastify's default error handler.

Full Integration Example

import Fastify from "fastify";
import { createSchedulingService } from "@forjakit/scheduling";
import { createPrismaSchedulingStorage } from "@forjakit/scheduling-prisma";
import { schedulingPlugin } from "@forjakit/scheduling-fastify";
import { PrismaClient } from "@prisma/client";
import { z } from "zod";

const prisma = new PrismaClient();
const app = Fastify({ logger: true });

const matchMetadata = z.object({
  round: z.number().int().positive(),
  group: z.string().optional(),
});

const service = createSchedulingService({
  storage: createPrismaSchedulingStorage(prisma.scheduledEvent),
  metadataSchema: matchMetadata,
});

app.register(schedulingPlugin, {
  service,
  prefix: "/api/scheduling",
  tenantResolver: (req) => {
    const tenantId = req.headers["x-tenant-id"] as string;
    if (!tenantId) throw new Error("Missing tenant");
    return tenantId;
  },
  guards: {
    write: async (req, reply) => {
      if (!req.headers.authorization) {
        reply.status(401).send({ error: "UNAUTHORIZED" });
      }
    },
  },
});

app.listen({ port: 3000 });

With this setup, the following routes are available:

POST   /api/scheduling/events
GET    /api/scheduling/events
GET    /api/scheduling/events/:id
PATCH  /api/scheduling/events/:id
DELETE /api/scheduling/events/:id
PATCH  /api/scheduling/events/:id/status
POST   /api/scheduling/events/:id/cancel

Exports

export { schedulingPlugin } from "@forjakit/scheduling-fastify";
export type { SchedulingPluginOptions } from "@forjakit/scheduling-fastify";