appRouter Overview

The appRouter in JSandy:

  • Routes requests to the procedure that handles them
  • Serves as an entry point to deploy your backend

JSandy File Structure

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

An appRouter knows all the routes and their handlers; deploying your backend is as easy as deploying the appRouter. Since JSandy is built on top of Hono, you can deploy this router anywhere: Vercel, Netlify, Cloudflare, AWS, Railway, etc.


Creating the appRouter

An appRouter is built on top of a base api that defines global behaviors. For example, where your API is served from, error handling, 404 response handling, or global middleware:

TypeScript

server/index.ts

import { j } from "./jsandy"
import { postRouter } from "./routers/post-router"
 
// 1️⃣ Creating the base API with global configuration
const api = j
  .router()
  .basePath("/api")
  .use(j.defaults.cors)
  .onError(j.defaults.errorHandler)
 
// 2️⃣ Merging with feature routers
const appRouter = j.mergeRouters(api, {
  post: postRouter,
})
 
export type AppRouter = typeof appRouter
export default appRouter

Configuration Options

All JSandy routers are lightweight, minimal extensions built on top of the Hono API. Any method you can call on the Hono API, you can also call on JSandy routers:

Here are the most common methods:


Error Handling

I recommend using JSandy's default error handler via j.defaults.errorHandler. It catches all router errors and returns standardized error responses that you can easily handle on the frontend.

TypeScript

server/index.ts

import { j } from "../jsandy"
 
const api = j
  .router()
  .basePath("/api")
  .use(j.defaults.cors)
  .onError(j.defaults.errorHandler)
TypeScript

app/page.tsx

"use client"
 
import { useMutation } from "@tanstack/react-query"
import { HTTPException } from "hono/http-exception"
import { client } from "@/lib/client"
 
export default function Page() {
  const { mutate: createPost } = useMutation({
    mutationFn: async () => {
      const res = await client.post.create.$post()
      return await res.json()
    },
    onError: (err: HTTPException) => {
      console.log(err.message)
    },
  })
 
  return <button onClick={() => createPost()}>Create Post</button>
}

You can also customize error handling if needed:

api.onError((err, c) => {
  console.error(`${err}`)
  return c.text("Custom Error Message", 500)
})

Base Path

To configure where your API is served from, adjust the basePath parameter and rename your api directory to match this new path:

// 👇 Serve all routes under /api/*
const api = j
  .router()
  .basePath("/api")
  .use(j.defaults.cors)
  .onError(j.defaults.errorHandler)
Change JSandy API path

CORS

Nobody likes CORS (🤮), but it's a necessary evil for security reasons. I recommend using JSandy's default CORS middleware:

TypeScript

server/index

import { InferRouterInputs, InferRouterOutputs } from "@jsandy/rpc"
import { j } from "./jsandy"
import { postRouter } from "./routers/post-router"
 
const api = j
  .router()
  .basePath("/api")
  .use(j.defaults.cors)
  .onError(j.defaults.errorHandler)

You can also customize CORS if needed using Hono's built-in CORS middleware. In this case, it's essential to whitelist the x-is-superjson header, as JStack uses this internally for c.superjson() responses:

import { cors } from "hono/cors"
 
cors({
  allowHeaders: ["x-is-superjson"],
  exposeHeaders: ["x-is-superjson"],
  origin: (origin) => origin, // default: allow any origin
  credentials: true, // default: allow credentials
})

Inferring Router Inputs/Outputs

import type { AppRouter } from "./jsandy"
import type { InferRouterInputs, InferRouterOutputs } from "@jsandy/rpc"
 
type InferInput = InferRouterInputs<AppRouter>
type InferOutput = InferRouterOutputs<AppRouter>
 
// 👇 Usage: InferInput[<router>][<procedure>]
type Input = InferInput["post"]["example"]
type Output = InferOutput["post"]["example"]