77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/src/server/routers/example.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is an example router, you can delete this file and then update `../pages/api/trpc/[trpc].tsx`
3 | */
4 | import { createId } from "@paralleldrive/cuid2";
5 | import { TRPCError } from "@trpc/server";
6 | import { sql } from "drizzle-orm";
7 | import { eq, gte } from "drizzle-orm/expressions";
8 | import { z } from "zod";
9 | import { db } from "~/db/drizzle-db";
10 | import { posts } from "~/db/schema";
11 | import { privateProcedure, publicProcedure, router } from "../trpc";
12 |
13 | export const exampleRouter = router({
14 | createPost: privateProcedure
15 | .input(
16 | z.object({
17 | text: z.string(),
18 | title: z.string(),
19 | }),
20 | )
21 | .mutation(async ({ ctx, input }) => {
22 | const userEmail = ctx.user.email;
23 | if (!userEmail) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
24 |
25 | await db.insert(posts).values({
26 | id: createId(),
27 | user_id: ctx.user.id,
28 | slug: createId(),
29 | title: input.title,
30 | text: input.text,
31 | });
32 | }),
33 |
34 | getPost: publicProcedure.input(z.object({ slug: z.string() })).query(async ({ input }) => {
35 | const files = await db
36 | .select({ title: posts.title, text: posts.text })
37 | .from(posts)
38 | .where(eq(posts.slug, input.slug))
39 | .limit(1);
40 | const file = files[0];
41 |
42 | // NOT_FOUND is fine if the file exists but the user doesn't have access to it. This prevents revealing that the file exists.
43 | if (!file) throw new TRPCError({ code: "NOT_FOUND" });
44 |
45 | return {
46 | title: file.title,
47 | text: file.text,
48 | };
49 | }),
50 |
51 | getInfinitePosts: publicProcedure
52 | .input(
53 | z.object({
54 | limit: z.number().min(1).max(100),
55 | cursor: z.date().nullish(), // <-- "cursor" needs to exist to create an infinite query, but can be any type
56 | }),
57 | )
58 | .query(async ({ input }) => {
59 | const limit = input.limit ?? 50;
60 |
61 | const countRows = await db.select({ files_count: sql`count(${posts.id})`.as("files_count") }).from(posts);
62 | const totalCount = countRows[0]?.files_count;
63 | if (totalCount === undefined) throw new Error("Failed to query total file count");
64 |
65 | let itemsQuery = db
66 | .select({ created_at: posts.created_at, slug: posts.slug, title: posts.title })
67 | .from(posts)
68 | .limit(input.limit);
69 | const cursor = input.cursor;
70 | if (cursor) {
71 | itemsQuery = itemsQuery.where(gte(posts.created_at, cursor));
72 | }
73 | const items = await itemsQuery.execute();
74 |
75 | let nextCursor: typeof input.cursor | undefined = undefined;
76 | if (items.length > limit) {
77 | // TODO: is this a safe assertion?
78 | const nextItem = items.pop() as NonNullable<(typeof items)[number]>;
79 | nextCursor = nextItem.created_at;
80 | }
81 |
82 | const returnableItems = items.map((item) => {
83 | return {
84 | title: item.title,
85 | created_at: item.created_at,
86 | slug: item.slug,
87 | };
88 | });
89 |
90 | return {
91 | items: returnableItems,
92 | nextCursor,
93 | totalCount,
94 | };
95 | }),
96 | });
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # T3 App Router (Edge)
2 |
3 | An experimental attempt at using the fantastic T3 Stack entirely on the Edge runtime, with Next.js's beta App Router.
4 |
5 | This is meant to be a place of hacking and learning. We're still learning how to structure apps using Next.js's new App Router, and comments are welcome in Discussions.
6 |
7 | If you encounter an error (you will), please create an Issue so that we can fix bugs and learn together.
8 |
9 | **This is not intended for production.** For a production-ready full-stack application, use the much more stable [create-t3-app](https://github.com/t3-oss/create-t3-app).
10 |
11 | This project is not affiliated with create-t3-app.
12 |
13 | ## Features
14 |
15 | This project represents the copy-pasting of work and ideas from a lot of really smart people. I think it's useful to see them all together in a working prototype.
16 |
17 | - Edge runtime for all pages and routes.
18 | - Type-safe SQL and schema management with drizzle-orm.
19 | - While create-t3-app uses Prisma, Prisma can't run on the Edge runtime.
20 | - Type-safe API with tRPC.
21 | - App Router setup is copied from [here](https://github.com/trpc/next-13).
22 | - The installed tRPC version is currently locked to the experimental App Router tRPC client in `./src/trpc/@trpc`, which formats the react-query query keys in a specific way that changed in later versions of tRPC. If you upgrade tRPC, hydration will stop working.
23 | - Owned Authentication with Auth.js.
24 | - create-t3-app uses NextAuth, which doesn't support the Edge runtime. This project uses NextAuth's successor, Auth.js, which does. Since Auth.js hasn't built support for Next.js yet, their [SolidStart implementation](https://github.com/nextauthjs/next-auth/tree/36ad964cf9aec4561dd4850c0f42b7889aa9a7db/packages/frameworks-solid-start/src) is copied and slightly modified.
25 | - Styling with [Tailwind](https://tailwindcss.com/).
26 | - It's just CSS, so it works just fine in the App Router.
27 | - React components and layout from [shadcn/ui](https://github.com/shadcn/ui)
28 | - They're also just CSS and Radix, so they work just fine in the App Router.
29 |
30 | ## Data Fetching
31 |
32 | There are a few options that Server Components + tRPC + React Query afford us. The flexibility of these tools allows us to use different strategies for different cases on the same project.
33 |
34 | 1. Fetch data on the server and render on the server or pass it to client components. [Example.](https://github.com/mattddean/t3-app-router-edge/blob/03cd3c0d16fb08a208279e08d90014e8e4fc8322/src/app/profile/page.tsx#L14)
35 | 1. Fetch data on the server and use it to hydrate react-query's cache on the client. Example: [Fetch and dehydrate data on server](https://github.com/mattddean/t3-app-router-edge/blob/c64d8dd8246491b7c4314c764b13d493b616df09/src/app/page.tsx#L19-L39), then [use cached data from server on client](https://github.com/mattddean/t3-app-router-edge/blob/03cd3c0d16fb08a208279e08d90014e8e4fc8322/src/components/posts-table.tsx#L84-L87).
36 | 1. Fetch data on the client.
37 | 1. Fetch data the server but don't block first byte and stream Server Components to the client using a Suspense boundary. TODO: Example.
38 |
39 | ## Getting Started
40 |
41 | 1. Run some commands.
42 |
43 | ```sh
44 | pnpm i
45 | cp .env.example .env
46 | ```
47 |
48 | 2. Fill in [.env](./.env).
49 |
50 | 3. Push your schema changes to a new PlanetScale database. Don't use this command on an existing database that you care about. It's destructive (and in beta).
51 |
52 | ```sh
53 | pnpm db:push
54 | ```
55 |
56 | 4. Start the Next.js dev server.
57 |
58 | ```sh
59 | pnpm dev
60 | ```
61 |
--------------------------------------------------------------------------------
/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | ChevronLeft,
5 | ChevronRight,
6 | Loader2,
7 | LogOut,
8 | User,
9 | X,
10 | type Icon as LucideIcon,
11 | type LucideProps,
12 | } from "lucide-react";
13 | import { type FC } from "react";
14 |
15 | export type Icon = LucideIcon;
16 |
17 | const Logo: FC = (props) => (
18 |