For simplicity, I recommend defining procedures and middleware in the jsandy.ts file. You can create a separate file if necessary.
Procedures Overview
By default, JSandy provides a publicProcedure that anyone, authenticated or not, can call. It serves as a base from which to build new procedures:
server/jsandy.ts
import { jsandy } from "@jsandy/rpc"interface Env { Bindings: { DATABASE_URL: string }}export const j = jsandy.init<Env>()/** * Public (unauthenticated) procedures * This is the base part you use to create new procedures. */export const publicProcedure = j.procedure
import { jsandy } from "@jsandy/rpc"interface Env { Bindings: { DATABASE_URL: string }}export const j = jsandy.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/jsandy.ts
import { HTTPException } from "hono/http-exception"import { jsandy } from "@jsandy/rpc"interface Env { Bindings: { DATABASE_URL: string }}export const j = jsandy.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.procedureexport const privateProcedure = publicProcedure.use(authMiddleware)
import { HTTPException } from "hono/http-exception"import { jsandy } from "@jsandy/rpc"interface Env { Bindings: { DATABASE_URL: string }}export const j = jsandy.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.procedureexport const privateProcedure = publicProcedure.use(authMiddleware)
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 "../jsandy"export const postRouter = j.router({ recent: publicProcedure.get(({ c, ctx, input }) => { const post = { id: 1, title: "My first post", } return c.json({ post }) }),})
import { j, publicProcedure } from "../jsandy"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()
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
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()
import { client } from "@/lib/client"const res = await client.post.create.$post()
Input Validation
JSandy 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 "../jsandy"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}"` }) }),})
import { z } from "zod"import { j, publicProcedure } from "../jsandy"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 inputawait client.post.create.$post({ title: "My new post" })
import { client } from "@/lib/client"// ✅ Client knows that `title` is expected inputawait 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
WebSocket procedures are serverless, with no additional infrastructure management. To make this possible, JSandy uses Upstash Redis as its real-time engine and expects WebSocket procedures to be deployed to Cloudflare Workers.