jstack icon indicating copy to clipboard operation
jstack copied to clipboard

Any way to integrate Hono-OpenAPI with JStack?

Open Blankeos opened this issue 5 months ago • 4 comments

https://hono.dev/examples/hono-openapi

I tried:

export contst postRouter = j.router({
  recent: publicProcedure
    .use(
      j.fromHono(
        describeRoute({
          description: "Awesome stuff",
          responses: {
            200: {
              description: "Successful response",
            },
          },
        }),
      ),
    )
    .query(({ c }) => {
      return c.superjson(posts.at(-1) ?? null);
    }),
// ...

and

const app = j.router();

app
  .get(
    "/api/docs/json",
    openAPISpecs(app, {
      documentation: {
        info: {
          title: "Hono API",
          version: "1.0.0",
          description: "JStack API",
        },
        servers: [
          { url: "http://localhost:3000", description: "Local Server" },
        ],
      },
    }),
  )
  .get(
    "/api/docs",
    Scalar((c) => {
      return {
        url: "/doc",
      };
    }),
  )
  .basePath("/api")
  .use(j.defaults.cors)
  .onError(j.defaults.errorHandler);

import { postRouter } from "./routers/post-router";

/**
 * This is the main router for your server.
 * All routers in /server/routers should be added here manually.
 */
const appRouter = j.mergeRouters(app, {
  post: postRouter,
});
export type AppRouter = typeof appRouter;

export default app;

I was really hoping it would work lol. Would be cool if Jstack supported it natively though. I'm seeing a usecase to have OpenAPI so maybe it's possible to generate a typesafe client on other languages (i.e. Dart/Flutter)

Blankeos avatar Jul 15 '25 04:07 Blankeos

You're intercepting the API route before its initialization, ideally you achieve your intended result like below

const api = jstack
  .router()
  .basePath("/api")
  .use(kuso.defaults.cors)
  .onError(kuso.defaults.errorHandler)

// For some reason placing anything before  .basePath("/api") tends to not work not always but most of the times have tried it

api.get(
    "/docs/json",
    openAPISpecs(app, {
      documentation: {
        info: {
          title: "Hono API",
          version: "1.0.0",
          description: "JStack API",
        },
        servers: [
          { url: "http://localhost:8000", description: "Local Server" },
        ],
      },
    }),
  )
api.get(
    "/docs/references",
    Scalar((c) => {
      return {
        url: "/doc",
      };
    }),
  )

la-niina avatar Jul 16 '25 23:07 la-niina

Thanks for the reply @la-niina ! I have attempted your solution. It doesn't work but I don't think the "setup" (second code snippet I posted that your comment tries to fix) is the issue.

I think it's the describeRoute() not working when used in .use() at the procedure level. So openApiSpecs() is empty and doesn't see the route.

Silly of me to not give a repro. Here it is: https://github.com/Blankeos/jstack-demo.git

For instance. In regular hono:

  api.get(
    "/docs",
    Scalar((c) => {
      return {
        url: "/doc",
      };
    }),
  )
  // Negative Case: Using `.use()` does not work.
  .use(
    describeRoute({
      description:
        "This description will not be visible because it's not inside .get()",
    }),
  )
  // Positive Case: Passing describeRoute() inside .get() works.
  .get(
    "/status",
    describeRoute({
      description: "Checks if the API is up.",
    }),
    async (c) => {},
  );

And in a jstack procedure this is my attempt to imitate it (which unfortunately does not work):

  recent: publicProcedure
    .use(
      j.fromHono(
        describeRoute({
          description: "This returns the most recent post.",
        }),
      ),
    )
    .use(async ({ ctx, next }) => {
      await next();
    })
    .query(async ({ c }) => {
      return c.superjson(posts.at(-1) ?? null);
    }),

Blankeos avatar Jul 17 '25 02:07 Blankeos

complete, The main issue was that you were adding routes to the base app instead of the merged appRouter, and the Scalar documentation wasn't pointing to the correct OpenAPI spec URL.

After reading you comment again I think I forgot about the use part silly me le me definitely rest for now

import { Scalar } from "@scalar/hono-api-reference";
import { describeRoute, openAPISpecs } from "hono-openapi";
import { j } from "./jstack";
import { postRouter } from "./routers/post-router";

/**
 * This is your base API.
 * Here, you can handle errors, not-found responses, cors and more.
 *
 * @see https://jstack.app/docs/backend/app-router
 */
const app = j
  .router()
  .basePath("/api")
  .use(j.defaults.cors)
  .onError(j.defaults.errorHandler);


/**
 * This is the main router for your server.
 * All routers in /server/routers should be added here manually.
 */
const appRouter = j.mergeRouters(app, {
  post: postRouter,
});
export type AppRouter = typeof appRouter;

// Add routes to the main app router, not the base app
appRouter
  .get(
    "/docs/json",
    openAPISpecs(appRouter, {
      documentation: {
        info: {
          title: "Hono API",
          version: "1.0.0",
          description: "JStack API",
        },
        servers: [
          { url: "http://localhost:3000", description: "Local Server" },
        ],
      },
    }),
  )
  .get(
    "/docs",
    Scalar((c) => {
      return {
        url: "/api/docs/json", // Fixed: Full path to the JSON endpoint
      };
    }),
  )
  .get(
    "/status",
    describeRoute({
      description: "Checks if the API is up.",
    }),
    async (c) => {
      // Added actual response
      return c.json({ status: "ok", timestamp: new Date().toISOString() });
    },
  );

// Export the merged router, not the base app
export default appRouter;

la-niina avatar Jul 17 '25 04:07 la-niina

Ah right, that's my bad. Thanks for looking into it btw, have a good rest!

I didn't really look at /docs that much, just /docs/json. Though the issue remains with describeRoute() in the post-router.ts currently. /docs/json just isn't seeing it.

Blankeos avatar Jul 17 '25 04:07 Blankeos