Introduction to Hono RPC for Next.Js Development

This article concerns with hono RPC and Next.Js for typesafe API routes

Why care about Hono RPC?

Hono, is a fast and lightweight web framework made to run on multiple runtimes. Originally made for cloudfare workers, now also support various if not all web frameworks and runtimes. But the question remains the same, why even need hono RPC? Well the reality of situation is API routes are kinda vague, just like replies from your crush. One can validate the requests and responses but that doesn’t change the fact that we have no type safety for the API routes.

The RPC feature allows sharing of the API specifications between the server and the client.

In short one can make scalable and type safe API routes with all the production ready features

Setting up Hono Rpc for Next.Js

Before we proceed, i would like to provide all the necessary links from the documentation for cross referencing. You can also leverage libraries such as react-query for efficient API calls

💡
This tutorial uses appwrite as the backend service
💡
The below written code assumes that you already have routing knowledge and willingness to read the hono documentation
  1. Ofc first install hono with your favorite package manager

  2. In src/app, make a catch all route [[...route]]/route.ts , assuming you are using app router

     import { Hono } from 'hono'
     import { handle } from 'hono/vercel'
    
     export const runtime = 'edge'
    
     const app = new Hono().basePath('/api')
    
     app.get('/hello', (c) => {
       return c.json({
         message: 'Hello Next.js!',
       })
     })
    
     export const GET = handle(app)
     export const POST = handle(app)
    
  3. Make a new folder such as features in the source directory src/features

    🚧
    Make sure to make this folder outside of the app folder as otherwise it may cause build errors

    Here’s one of the auth feature example

    • inside features folder, make an auth directory features/auth

    • inside auth make an api folder auth/api, in this api folder, make a login file, namely use-login.ts

        import { useMutation, useQueryClient } from "@tanstack/react-query"; // react query 
        import { InferRequestType, InferResponseType } from "hono";
        import { toast } from "sonner"; // toast library
      
        import { client } from "@/lib/rpc";
        import { useRouter } from "next/navigation";
      
        type ResponseType = InferResponseType<(typeof client.api.auth.login)["$post"]>;
        type RequestType = InferRequestType<(typeof client.api.auth.login)["$post"]>;
      
        export function Login() {
          const router = useRouter();
          const queryClient = useQueryClient();
      
          const mutation = useMutation<ResponseType, Error, RequestType>({
            mutationFn: async ({ json }) => {
              const res = await client.api.auth.login.$post({ json });
              if (!res.ok) throw new Error("Failed to login");
              return await res.json();
            },
            onSuccess: () => {
              toast.success("Logged In");
              router.refresh();
              queryClient.invalidateQueries({ queryKey: ["current"] });
            },
            onError: () => {
              toast.error("Error while logging in, plesase try again");
            },
          });
      
          return mutation;
        }
      
    • inside the same auth folder make another folder namely server with a file server/route.ts

        import { Hono } from "hono";
        import { zValidator } from "@hono/zod-validator";
        import { ID } from "node-appwrite";
        import { deleteCookie, setCookie } from "hono/cookie";
      
        import { loginSchema, registerSchema } from "@/lib/schemas/auth-schema";
        import { createAdminClient } from "@/lib/appwrite";
        import { AUTH_COOKIE } from "@/lib/constants";
        import { sessionMiddleware } from "@/lib/middlewares/session-middleware";
      
        const app = new Hono()
          .get("/current", sessionMiddleware, async (c) => {
            const user = c.get("user");
      
            return c.json({ data: user });
          })
          .post("/login", zValidator("json", loginSchema), async (c) => {
            const { email, password } = c.req.valid("json");
            const { account } = await createAdminClient();
      
            const session = await account.createEmailPasswordSession(email, password);
            setCookie(c, AUTH_COOKIE, session.secret, {
              path: "/",
              httpOnly: true,
              secure: true,
              sameSite: "strict",
              maxAge: 60 * 60 * 24 * 30,
            });
      
            return c.json({ message: "success" });
          })
          .post("/register", zValidator("json", registerSchema), async (c) => {
            const { account } = await createAdminClient();
            const { email, password, name } = c.req.valid("json");
            setCookie(c, AUTH_COOKIE, session.secret, {
              path: "/",
              httpOnly: true,
              secure: true,
              sameSite: "strict",
              maxAge: 60 * 60 * 24 * 30,
            });
      
            return c.json({ success: true });
          })
          .post("/logout", sessionMiddleware, async (c) => {
            const account = c.get("account");
      
            deleteCookie(c, AUTH_COOKIE);
            await account.deleteSession("current");
      
            return c.json({ success: true });
          });
      
        export default app;
      
  4. And looks like you are all done, now you just need to make call to the backend

     "use client";
    
     import Image from "next/image";
     import { Eye, EyeClosed, Github } from "lucide-react";
     import { useForm } from "react-hook-form";
     import { zodResolver } from "@hookform/resolvers/zod";
     import { useState } from "react";
    
     import { TextDivider } from "@/components/text-divider";
     import { Button } from "@/components/ui/button";
     import {
       Card,
       CardContent,
       CardDescription,
       CardHeader,
       CardTitle,
     } from "@/components/ui/card";
     import { Input } from "@/components/ui/input";
     import { Separator } from "@/components/ui/separator";
     import { loginSchema, loginValues } from "@/lib/schemas/auth-schema";
     import {
       Form,
       FormControl,
       FormField,
       FormItem,
       FormLabel,
       FormMessage,
     } from "@/components/ui/form";
     import { Login } from "@/features/auth/api/use-login";
    
     export function SignInCard() {
       const { mutate, isPending } = Login();
       const [showPassword, setShowPassword] = useState(false);
       const form = useForm<loginValues>({
         defaultValues: {
           email: "",
           password: "",
         },
         resolver: zodResolver(loginSchema),
       });
    
       function onSubmit(values: loginValues) {
         mutate({ json: values });
       }
    
       return (
         <Card className="h-full w-full border-none shadow-none md:w-[487px]">
           <CardHeader className="flex items-center justify-center p-7 text-center">
             <CardTitle className="capitalize">Welcome Back</CardTitle>
           </CardHeader>
           <div className="px-7">
             <Separator />
           </div>
           <CardContent className="p-7">
             <Form {...form}>
               <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
                 <FormField
                   control={form.control}
                   name="email"
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>Email</FormLabel>
                       <FormControl>
                         <Input placeholder="your account email" {...field} />
                       </FormControl>
                       <FormMessage />
                     </FormItem>
                   )}
                 />
    
                 <FormField
                   control={form.control}
                   name="password"
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>Password</FormLabel>
                       <FormControl>
                         <div className="flex justify-between gap-2">
                           <Input
                             type={showPassword ? "text" : "password"}
                             placeholder="your account password"
                             className=""
                             {...field}
                           />
                           <button
                             type="button"
                             onClick={() => setShowPassword(!showPassword)}
                           >
                             {showPassword ? (
                               <Eye className="size-5 text-neutral-600" />
                             ) : (
                               <EyeClosed className="size-5 text-neutral-600" />
                             )}
                           </button>
                         </div>
                       </FormControl>
                       <FormMessage />
                     </FormItem>
                   )}
                 />
                 <Button
                   disabled={isPending}
                   size="lg"
                   className="w-full"
                   type="submit"
                 >
                   Submit
                 </Button>
               </form>
             </Form>
           </CardContent>
    
           <div className="px-7">
             <TextDivider text="Other login methods" />
           </div>
           <CardContent className="flex flex-col gap-y-4 p-7">
             <Button
               className="w-full"
               variant="outline"
               size="lg"
               disabled={isPending}
             >
               <Image
                 src="/google.svg"
                 height={25}
                 width={25}
                 alt="google"
                 className="mt-0.5"
               />
               Login with google
             </Button>
             <Button
               className="w-full"
               variant="outline"
               size="lg"
               disabled={isPending}
             >
               <Github />
               Login with github
             </Button>
           </CardContent>
         </Card>
       );
     }
    

Resources

This video provides great tutorial and extensive setup of the this paradigm