vishalbalaji / trpc-svelte-query-adapter

A simple adapter to use `@tanstack/svelte-query` with trpc, similar to `@trpc/react-query`.
71 stars 7 forks source link

Instructions to setup with api url #42

Closed cellulosa closed 3 months ago

cellulosa commented 4 months ago

In case anybody is interested in using this with the createTRPCProxyClient object on SvelteKit to expose an /api/trpc/ route... This is how I've done it

// $lib/trpc.ts
import type { AppRouter } from '$lib/server/trpc/routes';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import SuperJSON from 'superjson';
import { svelteQueryWrapper } from 'trpc-svelte-query-adapter';
import type { QueryClient } from '@tanstack/svelte-query';

const client = createTRPCProxyClient<AppRouter>({
    links: [
        httpBatchLink({
            url: 'http://localhost:5173/api/trpc'
        })
    ],
    transformer: SuperJSON
});

export const trpc = (queryClient?: QueryClient) =>
    svelteQueryWrapper<AppRouter>({
        client,
        queryClient
    });
// $lib/server/trpc/index.ts
import { initTRPC } from '@trpc/server';
import type { Context } from '$lib/server/trpc/context';
import superjson from 'superjson';
import { ZodError } from 'zod';

const t = initTRPC.context<Context>().create({
    transformer: superjson
});
export const { procedure, router, middleware, createCallerFactory } = t;
// $lib/server/trpc/context.ts
import type { RequestEvent } from '@sveltejs/kit';
import type { inferAsyncReturnType } from '@trpc/server';

export const createContext = async (event: RequestEvent) => ({ event });
export type Context = inferAsyncReturnType<typeof createContext>;
// $lib/server/trpc/caller.ts
import { createCallerFactory } from '$lib/server/trpc';
import { appRouter } from '$lib/server/trpc/routes';

export const createCaller = createCallerFactory(appRouter);
// $lib/server/trpc/routes/index.ts
import { clients } from './clients';
import { files } from './files';
import { projects } from './projects';
import { users } from './users';
import { router } from '$lib/server/trpc';

export const appRouter = router({
    users
});

export type AppRouter = typeof appRouter;
// $lib/server/trpc/routes/users.ts
import { isLogged } from '$lib/server/trpc/middlewares/auth';
import { router, procedure } from '$lib/server/trpc';
import { db } from '$lib/server/database/index';
import { client } from '$lib/server/database/schema';
import { eq } from 'drizzle-orm';
import { clientSchema } from '$lib/zod';

export const users = router({
    list: procedure
        // use query for GET
        .query(async () => db.select().from(client)),
    delete: procedure
        .use(isLogged)
        .input(userSchema.pick({ id: true }))
        // use mutation for POST
        .mutation(async ({ input: { id } }) => {
            db.delete(user).where(eq(user.id, id));
        })
});
// routes/api/trpc/[...path]/+server.ts
import { createContext } from '$lib/server/trpc/context';
import { appRouter } from '$lib/server/trpc/routes';
import type { RequestHandler } from '@sveltejs/kit';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';

const requestHandler: RequestHandler = (event) =>
    fetchRequestHandler({
        req: event.request,
        router: appRouter,
        endpoint: '/api/trpc',
        createContext: () => createContext(event)
    });

export const GET = requestHandler;
export const POST = requestHandler;
// routes/+layout.ts
import { browser } from '$app/environment';
import { QueryClient } from '@tanstack/svelte-query';
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async () => {
    const queryClient = new QueryClient({
        defaultOptions: {
            queries: {
                enabled: browser,
                staleTime: 60 * 1000
            }
        }
    });

    return { queryClient };
};
// routes/+layout.svelte
<script lang="ts">
    import { QueryClientProvider } from '@tanstack/svelte-query';
    import type { PageData } from './$types';

    export let data: PageData;
</script>

<QueryClientProvider client={data.queryClient}>
    <main>
        <slot />
    </main>
</QueryClientProvider>

I am using Lucia so that's why I have the ctx.event.locals.user and session in the auth middleware to stop non-logged in users acceessing certain routes.

// $lib/server/trpc/middlewares/auth.ts
import { middleware } from '$lib/server/trpc';
import { TRPCError } from '@trpc/server';

export const isLogged = middleware(async ({ next, ctx }) => {
    if (!ctx.event.locals.user && !ctx.event.locals.session)
        throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authorized!' });
    return next();
});

Bonus in case you're interested to see what's in the Zod user schema.

// $lib/zod.ts
import { z } from 'zod';
import validator from 'validator';

export const roles = ['admin', 'user'] as const;
export const roleEnumSchema = z.enum(roles);
export const userSchema = z.object({
    id: z.string()
    role: roleEnumSchema,
    username: z
        .string()
        .min(3)
        .max(31)
        .refine((val) => validator.isAlphanumeric(val), {
            message: 'Username can only contain letters and numbers.'
        }),
    password: z.string().min(3).max(31)
});
export type User = z.infer<typeof userSchema>;
vishalbalaji commented 3 months ago

Hey @cellulosa, thank you for sharing this!