Procedures

A procedure in JStack is an API endpoint that handles a specific operation. There are three kinds of procedures:

  • get procedures for GET requests
  • post procedures for POST requests
  • ws (WebSocket) procedures for real-time communication

JStack File Structure

app/
  └── server/
      ├── jstack.ts        # Initialize JStack
      ├── index.ts         # Main appRouter
      └── routers/         # Router directory
          ├── user-router.ts
          ├── post-router.ts
          └── payment-router.ts

For simplicity, I recommend defining procedures and middleware in the jstack.ts file. You can create a separate file if necessary.


Procedures Overview

By default, JStack provides a publicProcedure that anyone, authenticated or not, can call. It serves as a base from which to build new procedures:

server/jstack.ts

import { jstack } from "jstack"
 
interface Env {
  Bindings: { DATABASE_URL: string }
}
 
export const j = jstack.init<Env>()
 
/**
 * Public (unauthenticated) procedures
 * This is the base part you use to create new procedures.
 */
export const publicProcedure = j.procedure

Example Procedure

Let's create a procedure that only authenticated users can call:

server/jstack.ts

import { HTTPException } from "hono/http-exception"
import { jstack } from "jstack"
 
interface Env {
  Bindings: { DATABASE_URL: string }
}
 
export const j = jstack.init<Env>()
 
const authMiddleware = j.middleware(async ({ c, next }) => {
  // Mocked user authentication check...
  const isAuthenticated = true
 
  if (!isAuthenticated) {
    throw new HTTPException(401, {
      message: "Unauthorized, sign in to continue.",
    })
  }
 
  // 👇 Attach user to `ctx` object
  await next({ user: { id: "123", name: "John Doe" } })
})
 
/**
 * Public (unauthenticated) procedures
 * This is the base part you use to create new procedures.
 */
export const publicProcedure = j.procedure
export const privateProcedure = publicProcedure.use(authMiddleware)

Example Procedure Usage

server/routers/post-router.ts

import { j, privateProcedure } from "../jstack"
 
export const postRouter = j.router({
  list: privateProcedure.get(({ c }) => {
    return c.json({ posts: [] })
  }),
})

Tada! 🎉 Now only authenticated users can call our /api/post/list endpoint. Unauthenticated users will be rejected with a 401 response.


GET Procedures

GET procedures are used to read data from your API. They accept input via URL query parameters and use HTTP GET requests. Define them using the .get() method.

The handler receives the following objects:

  • c: Hono context, e.g. headers, request info, env variables
  • ctx: Your context, e.g. database instance, authenticated user
  • input: Validated input (optional)

server/routers/post-router.ts

import { j, publicProcedure } from "../jstack"
 
export const postRouter = j.router({
  recent: publicProcedure.get(({ c, ctx, input }) => {
    const post = {
      id: 1,
      title: "My first post",
    }
 
    return c.json({ post })
  }),
})

To call a GET procedure in your application, use your client's $get method:

page.tsx

import { client } from "@/lib/client"
 
const res = await client.post.recent.$get()

POST Procedures

POST procedures are used to modify, create or delete data. They accept input via the request body and use HTTP POST requests. Define them using the .post() method.

Like GET procedures, the handler receives the following objects:

  • c: Hono Context, e.g. headers, request info, env variables
  • ctx: Your context, e.g. database instance, authenticated user
  • input: Validated input (optional)

server/routers/post-router.ts

import { j, publicProcedure } from "../jstack"
 
export const postRouter = j.router({
  create: publicProcedure.post(({ c, ctx, input }) => {
    return c.json({ message: "Post created successfully!" })
  }),
})

To call a POST procedure in your application, use your client's $post method:

page.tsx

import { client } from "@/lib/client"
 
const res = await client.post.create.$post()

Input Validation

JStack has built-in runtime validation for user input using Zod. To set up an input validator, use the procedure.input() method:

server/routers/post-router.ts

import { z } from "zod"
import { j, publicProcedure } from "../jstack"
 
export const postRouter = j.router({
  create: publicProcedure
    .input(z.object({ title: z.string() }))
    .post(({ c, ctx, input }) => {
      // 👇 Guaranteed to exist & automatically typed
      const { title } = input
 
      return c.json({ message: `Created post: "${title}"` })
    }),
})

If an API request does not contain the expected input (either as a URL parameter for get() or as a request body for post()), your global onError will automatically catch this error for easy frontend handling.

Also, if you call this procedure from the client, you'll get immediate feedback about the expected input:

page.tsx

import { client } from "@/lib/client"
 
// ✅ Client knows that `title` is expected input
await client.post.create.$post({ title: "My new post" })

WebSocket Procedures

WebSocket procedures provide real-time bi-directional communication between the client and server. They are created using the ws() method and can specify schemas for incoming and outgoing messages.

The handler receives the following objects:

  • c: Hono context, e.g. headers, request info, env variables
  • ctx: Your context, e.g. database instance, authenticated user
  • io: Connection manager for sending messages to clients

server/routers/post-router.ts

import { j, publicProcedure } from "../jstack"
import { z } from "zod"
 
const incomingEvents = z.object({
  like: z.object({ username: z.string(), postId: z.string() }),
})
 
const outgoingEvents = z.object({
  like: z.object({ username: z.string() }),
})
 
export const postRouter = j.router({
  likes: publicProcedure
    .incoming(incomingEvents)
    .outgoing(outgoingEvents)
    .ws(({ c, ctx, io }) => {
      return {
        onConnect({ socket }) {
          socket.on("like", ({ username, postId }) => {
            console.log(`User "${username}" liked post with id "${postId}"`)
 
            // 👇 Send event to all connected clients
            io.to(postId).emit("like", { username })
          })
        },
        onDisconnect({ socket }) {
          console.log("User disconnected")
        },
        onError({ socket, error }) {
          console.log("Socket error:", error)
        },
      }
    }),
})

WebSocket procedures are serverless, with no additional infrastructure management. To make this possible, JStack uses Upstash Redis as its real-time engine and expects WebSocket procedures to be deployed to Cloudflare Workers.

→ JStack WebSocket Docs