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

@boon4681/giri

v0.0.3-alpha-11

Published

A stupid attempt from a stupid man who lack of foresight trying to make a backend framework.

Readme

Giri

A stupid attempt from a stupid man who lack of foresight trying to make a backend framework.

NPM Version

F*CK NPM PUBLISH i have to change this package name from guri to giri to @boon4681/giri because of the name collision and there is no contact about request a package name that hit npm stupid filter.

Why does giri exist?

Because I can, and I am too lazy to write an OpenAPI spec. Write handlers, return values. Giri infers the OpenAPI spec from the handlers, and generates types for params and openapi.json from them. Runs on Hono.

Status: early and experimental. Hono is the only adapter today; the API will change.

Install

yarn add @boon4681/giri hono @hono/node-server zod

hono, @hono/node-server, zod, valibot, and typescript are optional peers - install only what you use.

Quick start

npx giri init     # scaffold giri.config.ts + src/routes + tsconfig + .gitignore
npx giri sync     # generate .giri/ (manifest, param types, openapi.json)
npx giri serve    # sync, then run the dev server (watches src/ and re-syncs)

Then hit it:

curl http://localhost:3000/

Config

giri.config.ts is declarative - it is loaded at build time, so keep it cheap and free of side effects (no DB drivers here; see Lifecycle).

import { defineConfig } from "@boon4681/giri";
import { hono } from "@boon4681/giri/adapters/hono";

export default defineConfig({
    adapter: hono(),                       // required: the backend bridge
    server: { port: 3000, hostname: "127.0.0.1" },
    outDir: ".giri",                       // where generated output lives
    alias: { "$db": "./src/db.ts" },       // import aliases, also written into tsconfig
});

Routes

Every URL segment is a folder; every HTTP verb is its own file inside it.

src/routes/
  +get.ts                       -> GET    /
  +shared.ts                    -> folder config for everything below
  users/
    +get.ts                     -> GET    /users
    +post.ts                    -> POST   /users
    [id]/
      +get.ts                   -> GET    /users/:id
      posts/
        [postId]/
          +get.ts               -> GET    /users/:id/posts/:postId
  db.ts                         -> no '+' prefix = plain helper, ignored by the router
  • [id] folder becomes the param :id; params nest down the path.
  • Files without a + prefix are not routes - colocate helpers freely.

A verb file has one shape: the handle named export is the handler. Everything else is an optional named export, so the trivial case is one line and complexity is additive.

// src/routes/users/[id]/+get.ts
import type { Handle } from "./$types";    // generated per folder; binds c.params to this path
import { findUser } from "../../../db";

export const handle: Handle = (c) => {
    const user = findUser(c.params.id);    // c.params.id is typed as string
    if (!user) return c.json({ message: "user not found" }, 404);
    return c.json(user);
};
// inferred responses: 200 -> User, 404 -> { message: string }

The context c

Giri owns c, so the return type is the schema on every backend:

  • c.json(data, status?) / c.text(text, status?) - return value carries the status in its type.
  • c.params - typed from the folder path.
  • c.req.valid("body" | "query") - parsed, typed input (see below).
  • c.req.header(name), c.req.url, etc.
  • c.get(key) / c.set(key, value) - per-request vars from middleware.
  • c.app - app-wide services from src/main.ts init() (see Lifecycle).

Inputs

Outputs are inferred; inputs are declared with a wrapped schema so giri gets both runtime validation and a JSON Schema for the doc. Wrappers live in @boon4681/giri/validators/zod and @boon4681/giri/validators/valibot.

// src/routes/users/+post.ts
import { z } from "zod";
import { zod } from "@boon4681/giri/validators/zod";
import type { POST } from "./$types";

export const body = zod.body({
    json: z.object({ name: z.string().min(1) }),
});
export const query = zod.query(z.object({ page: z.coerce.number().default(1) }));

export const handle: POST = (c) => {
    const { name } = c.req.valid("body");  // typed + validated
    return c.json({ name }, 201);
};

zod.body can map multiple content types (json, form) dispatched on Content-Type at runtime. An unwrapped schema is rejected at build time.

Middleware

Middleware use giri's (c, next) shape and live in two places:

  • Broad: export const middleware in a folder's +shared.ts - applies to the whole subtree.
  • Precise: export const middleware in a verb file - applies to that one verb.

Use stack(...) instead of a plain array so injected vars keep their types and propagate to downstream handlers. Run order: inherited +shared.ts (root to leaf), then the verb's middleware, then the handler.

// src/routes/+shared.ts
import { stack } from "@boon4681/giri";
import type { Middleware } from "./$types";

const requestId: Middleware<{ requestId: string }> = async (c, next) => {
    c.set("requestId", c.req.header("x-request-id") ?? "example-request");
    await next();
};

export const middleware = stack(requestId);
// every handler below now sees c.get("requestId"): string

Tag a middleware with defineMiddleware to feed OpenAPI security automatically - a route that uses it shows the scheme, a public route does not.

// src/auth.ts
import { defineMiddleware } from "@boon4681/giri";

export const auth = defineMiddleware<{ userId: string }>(
    {
        openapi: {
            security: [{ bearerAuth: [] }],
            securitySchemes: { bearerAuth: { type: "http", scheme: "bearer" } },
        },
    },
    async (c, next) => {
        // verify a token, then c.set("userId", ...)
        await next();
    },
);

defineMiddleware can also own a body or query validator. Validation runs before the middleware, c.req.valid(...) is typed inside it, and every downstream handler receives the same typed input. The validator is also included in generated OpenAPI:

const pagination = defineMiddleware(
    {
        query: zod.query(z.object({
            page: z.coerce.number().int().positive(),
        })),
    },
    async (c, next) => {
        const { page } = c.req.valid("query");
        c.set("page", page);
        await next();
    },
);

When a middleware both injects context vars and owns a validator, declare the vars with the curried form - the empty () takes the Vars type argument first so the validator output is still inferred (a Vars argument on defineMiddleware(options, ...) directly would suppress inference and erase c.req.valid(...) to any, so that form is a type error):

const pagination = defineMiddleware<{ page: number }>()(
    {
        query: zod.query(z.object({ page: z.coerce.number().int().positive() })),
    },
    async (c, next) => {
        c.set("page", c.req.valid("query").page); // valid("query") is typed; c.get("page") is number
        await next();
    },
);

When several applied layers declare the same target (body or query) - e.g. a +shared.ts pagination middleware plus a route's own query - their validators are merged: each runs and the validated outputs are combined into the single c.req.valid(...) (and one set of OpenAPI parameters). Owners run middleware-first, then the route, so a route's fields win on key collisions. This mirrors the type layer, which intersects them. Validators are run independently, so a schema that rejects unknown keys (e.g. zod .strict()) should not be combined with another owner.

Hide a route or subtree from openapi.json with export const openapi = false (in a verb file or a +shared.ts). Hidden routes still serve normally.

Lifecycle

src/main.ts is the optional home for imperative startup - opening pools, validating env, graceful shutdown. Giri owns the serve and calls these hooks; the adapter still binds the port.

// src/main.ts
import type { Services } from "@boon4681/giri";

export const init = () => {
    // leave init unannotated - its return type is the source of truth for c.app
    return { db: connectDb(process.env.DATABASE_URL) };
};

export const teardown = (services: Services) => {
    return services.db.close();            // runs on SIGINT / SIGTERM
};

Flow: load main.ts -> await init() -> hold services -> adapter serves -> teardown on exit. init runs once and is not re-run on watch rebuilds. The returned object reaches every handler as a typed c.app, inferred from init's return - no declaration needed.

CLI

| Command | What it does | | ------------ | ---------------------------------------------------------------------------------- | | giri init | Scaffold giri.config.ts, a starter route, tsconfig paths, and .gitignore. | | giri sync | Scan src/routes and regenerate .giri/ (manifest, param types, openapi.json). | | giri serve | sync, run init(), then serve via the adapter. Watches src/ and re-syncs. | | giri build | Planned - currently a no-op. |

giri serve flags: --port <n>, --host <addr>, --no-watch.

Generated output (.giri/)

Everything derived lives in .giri/ at the project root: param .d.ts per route, the route manifest, and the assembled openapi.json. It is gitignored and rebuilt on demand - never edit it, only import from it.

Example

See example/ for a runnable Hono app:

cd example
yarn install
yarn sync
yarn dev

Portable runtime

@boon4681/giri/runtime is the portable composition layer for generated integrations. It does not implement routing itself: it accepts a complete GiriAdapter, creates that adapter's app, and registers statically imported route modules. Hono is the supported adapter today:

import { createApp } from "@boon4681/giri/runtime";
import { hono } from "@boon4681/giri/adapters/hono";
import * as rootShared from "./src/routes/+shared";
import * as usersGet from "./src/routes/users/+get";

export const app = createApp({
    adapter: hono(),
    services: { source: "playground" },
    routes: [
        {
            method: "GET",
            path: "/api/users",
            module: usersGet,
            shared: [rootShared],
        },
    ],
});

The result is a normal Fetch application:

const response = await app.fetch(new Request("https://example.test/api/users"));

app is the native Hono application, so SvelteKit or Next.js can forward a framework request directly to app.fetch(request). This is the low-level target for virtual modules such as @giri/project-1; the Vite integration should generate the route descriptor array.

The shipped Hono adapter also owns its Node server binding. A browser playground should provide a different GiriAdapter implementation whose createApp, register, fetch, and serve methods bind to the desired browser runtime; createApp itself does not special-case that environment.

License

MIT