DEV Community

Cover image for create-t3-app for vite and react
Dennis kinuthia
Dennis kinuthia

Posted on • Edited on

create-t3-app for vite and react

initialize the project

final code

Setup using Rakkasjs which is a full-stack vite react framework

pnpm create rakkas-app my-rakkas-app
Enter fullscreen mode Exit fullscreen mode

Install the other dependencies

hattip

what is hattip? ,
it's what powers the backend for Rakkasjs, and allows you to write code that'll run in multiple runtimes and environments, like node, bun,deno, and Cloudflare workers ...

pnpm install hattip @hattip/response @hattip/cookie http-status-codes devalue
Enter fullscreen mode Exit fullscreen mode

trpc

What is trpc? , An easy way to write typesafe APIs in typescript, collocated backend, and frontend share types and give you a typesafe RPC API

pnpm install @trpc/client @trpc/react-query @trpc/server superjson zod
Enter fullscreen mode Exit fullscreen mode

prisma

This one is optional as tprc doesn't necessarily need it , but we're trying to recreate the t3-stack which in Nextjs in vite + react, you can use whatever javascript orm / data source

pnpm install prisma
Enter fullscreen mode Exit fullscreen mode

Rakkasjs basics

What is rakkasjs? , The closest thing to a Nextjs on Vite.

To get started we'll create the trpc API endpoint which will be a catch-all route with the all verb.

To create API routes in Rakkasjs we put it in the src/routes/api directory, A simple API route would look something like this

// src/routes/api/index.api.ts
import { json } from "@hattip/response";
import { StatusCodes } from "http-status-codes";
import { RequestContext } from "rakkasjs";

export async function get(ctx: RequestContext) {
try {
    return json({ message: "welcome to rakkas root api" },{ status: StatusCodes.ACCEPTED });
    } catch (error) {
    return json(error, { status: StatusCodes.BAD_REQUEST });    }
}

export async function post(ctx: RequestContext) {
    const body = await ctx.request.json();
    try {
        return json({body },{ status: StatusCodes.ACCEPTED });
    } catch (error) {
        return json(error,{ status: StatusCodes.BAD_REQUEST });
    }
}

Enter fullscreen mode Exit fullscreen mode

we're going to be doing alot of relative imports so let's setup tsconfig aliases to point @/ to src/


{
  "compilerOptions": {
    "target": "es2020",
    "module": "ESNext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "types": ["vite/client"],


   "paths": {
      "@/*": ["src/*"]
    }



  }
}

Enter fullscreen mode Exit fullscreen mode

Our trpc catch-all route

// src/routes/api/trpc/[...trpc].api.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { RequestContext } from 'rakkasjs';
import { createTRPCContext } from '@/server/trpc';
import { appRouter } from '@/server/routes/root';

export async function all(ctx: RequestContext){
    return fetchRequestHandler({
        endpoint: '/api/trpc',
        req: ctx.request,
        router: appRouter,
        createContext:createTRPCContext,
    });
}
Enter fullscreen mode Exit fullscreen mode

The only difference between this and the nextjs create-t3-app is that we're using the fetchRequestHandler instead of the nextjs adapter

hattip like most of the other agnostic runtime servers relies on the standard fetch API.

trpc setup

We'll put all the trpc logic in the src/server directory.

first, we'll create the trpc context

export function createTRPCContext({ req, resHeaders }: FetchCreateContextFnOptions) {
  const user = { name: req.headers.get('username') ?? 'anonymous' };
  // return createInnerTRPCContext({});
  return { req, resHeaders, user };
}

Enter fullscreen mode Exit fullscreen mode

then we init trpc

const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});
Enter fullscreen mode Exit fullscreen mode

then we export the router and procedure

export const createTRPCRouter = t.router;
export const router = t.router
export const publicProcedure = t.procedure;
Enter fullscreen mode Exit fullscreen mode

Then we can now create the trpc router + it's endpoints

// src/server/routes/root.ts
import { createTRPCRouter, publicProcedure } from '../trpc';
import { helloRouter } from './hello';
//  to test using a REST API client use a . to access nested routes insteda of a slash
//ex: http://localhost:5173/api/trpc/welcome
//ex: http://localhost:5173/api/trpc/hello.wave

export const appRouter = createTRPCRouter({
  hello:helloRouter,
});

// export type definition of API
export type AppRouter = typeof appRouter;

Enter fullscreen mode Exit fullscreen mode
// src/server/routes/hello.ts
import { createTRPCRouter, publicProcedure } from "../trpc";
export const helloRouter = createTRPCRouter({
    hey: publicProcedure.query((opts) => {
        return `Hey there`;
    }),
    wave: publicProcedure.query(async(opts) => {
        return `πŸ™‹β€β™€οΈ`;
    }),

})
Enter fullscreen mode Exit fullscreen mode

With thhat done , our endppoints are ready to consume on the frontend

lets add some frontend dependancies

pnpm install @tanstack/react-query react-toastify lucide-react @unpic/react tailwind-merge
Enter fullscreen mode Exit fullscreen mode

Then we configure the trpc client in src/utils/trpc.ts

// src/utils/trpc.ts
import type { AppRouter } from '@/server/routes/root';
import { createTRPCReact } from '@trpc/react-query';


export const trpc = createTRPCReact<AppRouter>();

Enter fullscreen mode Exit fullscreen mode

Then we create a trpc client for the provider

// src/utils/client.ts
import { trpc } from "./trpc";
import { httpBatchLink } from "@trpc/react-query";
import superjson from "superjson";

const getBaseUrl = (url?:string) => {
    if (typeof window !== "undefined") return ""; // browser should use relative url
    const urlObj = new URL(url as string);
    if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
    return urlObj.origin;
};


export const trpcClient = (url?:string)=>{
   return trpc.createClient({
        links: [
            httpBatchLink({
                url:`${getBaseUrl(url)}/api/trpc`,
            }),
        ],
        transformer: superjson
    });
}

Enter fullscreen mode Exit fullscreen mode

Then we setup the stack react query provider

// src/routes/layout.tsx
const [queryClient] = useState(() => new QueryClient());
...
return(
       <trpc.Provider client={trpcClient()} queryClient={queryClient}>
       <QueryClientProvider client={queryClient}>
         <section className="min-h-screen h-full w-full  ">{children}</section>
       </QueryClientProvider>
     </trpc.Provider>
)
Enter fullscreen mode Exit fullscreen mode

Rakkasjs has layouts , like the ones in nextjs app router which lets you wrap multiple pages with a commone layout , the one at the root src/routes/layout.tsx wraps the whole app so we can put our providers there

Let's add some tailwind for styling too official guide

try

npx bonita add tailwind
Enter fullscreen mode Exit fullscreen mode

or manually

pnpm i -D tailwindcss postcss autoprefixer daisyui  tailwindcss-animate tailwind-scrollbar tailwindcss-elevation prettier-plugin-tailwindcss
Enter fullscreen mode Exit fullscreen mode
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Now add the tailwind config content paths

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

And include the base CSS in our layout

src/routes/index.css

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode
// src/routes/layout.tsx
import ./index.css

Enter fullscreen mode Exit fullscreen mode

and now we consume our trpc endpoint

// src/routes/index.page.tsx
import { trpc } from "@/utils/trpc";

export default function HomePage() {
    const query = trpc.hello.hey.useQuery();
    return (
        <main className="flex flex-col gap-2 items-center">
            <h1>Hello world!</h1>
            <h3>{query?.data}</h3>
        </main>
    );
}

Enter fullscreen mode Exit fullscreen mode

Taddah, Your app is now ready

Alt text

let's add the Prisma parts, first thing we need is a schema and the env variables
prisma/schema.prisma

 // This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

generator zod {
  provider = "zod-prisma-types"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  name      String
  email     String
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  title String
  body  String
}

Enter fullscreen mode Exit fullscreen mode
# .env
DATABASE_URL="file:./db.sqlite"
Enter fullscreen mode Exit fullscreen mode

then we run the generate command

npx prisma generate
npx prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

and now we create a helper function for the db

// src/server/db.ts
import { PrismaClient } from "@prisma/client";
import { envs } from "@/utils/env";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log:envs.DEV_MODE ? ["query", "error", "warn"] : ["error"],
      // env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  });

// if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
if (envs.PROD_MODE) globalForPrisma.prisma = prisma;

Enter fullscreen mode Exit fullscreen mode

and we add prisma into our trpc context

export function createTRPCContext({ req, resHeaders }: FetchCreateContextFnOptions) {
  const user = { name: req.headers.get('username') ?? 'anonymous' };
  // return createInnerTRPCContext({});
  return { req, resHeaders, user,prisma };
}
Enter fullscreen mode Exit fullscreen mode

We can now use it in our trpc routes

// src/server/posts.ts

import { createTRPCRouter, publicProcedure } from "../trpc";
import { z } from "zod";

export const PostSchema = z.object({
    id: z.string().cuid().optional(),
    createdAt: z.coerce.date().optional(),
    updatedAt: z.coerce.date().optional(),
    title: z.string(),
    body: z.string(),
})

export type Post = z.infer<typeof PostSchema>

export const postRouter = createTRPCRouter({
    //  get the full list
    getFullList: publicProcedure.query(({ctx}) => {
        const posts = ctx.prisma.post.findMany();
        return posts;
    }),

    create: publicProcedure
    .input(PostSchema)
    .mutation(({ctx, input}) => {
        const post = ctx.prisma.post.create({
            data: input,
        });
        return post;
    }),

    update: publicProcedure
    .input(PostSchema)
    .mutation(({ctx, input}) => {
        const post = ctx.prisma.post.update({ where: {id: input.id},data: input,});
        return post;
    }),
    delete: publicProcedure
    .input(z.object({id: z.string()}))
    .mutation(({ctx, input}) => {
        const post = ctx.prisma.post.delete({ where: {id: input.id}});
        return post;
    })
})

// src/server/root.ts
export const appRouter = createTRPCRouter({
  hello:helloRouter,
  posts:postRouter
});


Enter fullscreen mode Exit fullscreen mode

We can now consume it in the frontend

// src/routes/posts
  const query = trpc.posts.getFullList.useQuery();
  return (
    <main className="flex flex-col gap-2 items-center">
      <h1>Hello world!</h1>
      <h3>{query?.data.map(...)}</h3>
    </main>
  );

Enter fullscreen mode Exit fullscreen mode

That's all you need to get started, but we can optimize this further
For starters, we're not taking full advantage of the SSR step to preload the data.

Rakkasjs hasit;s built-in useQuery and useServerSideQuery hooks to preload the data during the SSR step.

but since we're using tanstack react query we can use the integration guide the creator put out that lets you prefetch the data in the SSR step

we do this by adding Rakkasjs hoos hooks

  • client-entry.tsx : Code here will run on/wrap every page We can move the providers from the layout to here
/* eslint-disable no-var */
import { startClient } from "rakkasjs";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { trpc } from "./utils/trpc";
import { trpcClient } from "./utils/client";


const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      staleTime: 100,
      refetchOnWindowFocus: false,
      refetchOnReconnect: false,
    },
  },
});

function setQueryData(data: Record<string, unknown>) {
  for (const [key, value] of Object.entries(data)) {
    queryClient.setQueryData(JSON.parse(key), value, { updatedAt: Date.now() });
  }
}

declare global {
  var $TQD: Record<string, unknown> | undefined;
  var $TQS: typeof setQueryData;
}

// Insert data that was already streamed before this point
setQueryData(globalThis.$TQD ?? {});
// Delete the global variable so that it doesn't get serialized again
delete globalThis.$TQD;
// From now on, insert data directly
globalThis.$TQS = setQueryData;

startClient({
  hooks: {
    wrapApp(app) {
      return (
            <trpc.Provider client={trpcClient()} queryClient={queryClient}>
              <QueryClientProvider client={queryClient}>{app}</QueryClientProvider>
            </trpc.Provider>
      );
    },
  },
});

Enter fullscreen mode Exit fullscreen mode
  • hattip-entry.tsx We also need to create server-side providers
import { RequestContext, createRequestHandler } from "rakkasjs";
import { cookie } from "@hattip/cookie";

import {
  QueryCache,
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import { uneval } from "devalue";
import { trpc } from "./utils/trpc";
import { trpcClient } from "./utils/client";

declare module "rakkasjs" {
  interface ServerSideLocals {
    session: any;
  }
}

type CreateRequestHandlerParams = Parameters<typeof createRequestHandler>;

const attachSession = async (ctx: RequestContext) => {
  try {
    // ctx.locals.session = await getSession(
    //     ctx.platform.request,
    //     ctx.platform.response
    // );

    ctx.locals.session = {
      user: "jeffery",
    };
  } catch (error) {
    throw new Error("Failed to attach session");
  }
};
const logger = async (ctx: RequestContext) => {
  try {
    console.log("========", ctx.ip, "=============");
  } catch (error) {
    throw new Error("Failed to attach session");
  }
};

export default createRequestHandler({
  middleware: {
    // HatTip middleware to be injected
    // before the page routes handler.
    // @ts-expect-error
    beforePages: [cookie(), attachSession],
    // HatTip middleware to be injected
    // after the page routes handler but
    // before the API routes handler
    beforeApiRoutes: [logger],
    // HatTip middleware to be injected
    // after the API routes handler but
    // before the 404 handler
    beforeNotFound: [],
  },

  createPageHooks(ctx) {
    let queries = Object.create(null);
    console.log("hattip ctx", ctx.request.url);
    return {
      wrapApp(app) {
        const queryCache = new QueryCache({
          onSuccess(data, query) {
            // Store newly fetched data
            queries[query.queryHash] = data;
          },
        });

        const queryClient = new QueryClient({
          queryCache,
          defaultOptions: {
            queries: {
              suspense: true,
              staleTime: Infinity,
              refetchOnWindowFocus: false,
              refetchOnReconnect: false,
            },
          },
        });

        return (
          <trpc.Provider
            client={trpcClient(ctx.request.url)}
            queryClient={queryClient}
          >
            <QueryClientProvider client={queryClient}>
              {app}
            </QueryClientProvider>
          </trpc.Provider>
        );
      },

      emitToDocumentHead() {
        return `<script>$TQD=Object.create(null);$TQS=data=>Object.assign($TQD,data);</script>`;
      },

      emitBeforeSsrChunk() {
        if (Object.keys(queries).length === 0) return "";

        // Emit a script that calls the global $TQS function with the
        // newly fetched query data.

        const queriesString = uneval(queries);
        queries = Object.create(null);
        return `<script>$TQS(${queriesString})</script>`;
      },
    };
  },
});

Enter fullscreen mode Exit fullscreen mode
  • src/common-hooks.tsx : is also an option but we won't use it here

The hooks also allow you to define middleware and locals for things like sessions which can be shared between pages and API routes

That's it, there's also a CLI,
simply run

npx bonita create
Enter fullscreen mode Exit fullscreen mode

Top comments (0)