What I learned developing a full application for the first time.
Written by
Michael Silva
Published on
Last May I bought a Bambu Labs
A1. Mainly as a hobby and to make some parts for some home projects. While browsing for interesting creations from the community on Makerworld, I had the idea of creating myself a web app to store personal projects to reference and save notes and images of the progress.
I am a relatively new developer, so planning larger projects like this is a new experience. I started with a basic database schema planning. I found that for me its always a good place to start. It allows me to get an idea of the data and how interaction with that data will happen.
Next I gave myself a good starting point to quickly setup and hit the ground running. I found that the faster I can get something working the better for my ADHD. So I used the T3 stack to give me a good head start.
1pnpm create t3-app@latest
Options used:
Continuing that trend I added some basic shadcn components to get started on the UI. In just a short period of time I had a half decent looking app. But that was the easy part.
So UI being functional enough, I started digging into the api/server side of things. I set up the Drizzle
schema and tRPC
routes. Sure I may have needed the tRPC
and Drizzle
docs open the entire time. But hey, that is what they are for.
My first real hurdle was about now. As usual, I was starting to over think the schema and layout and whatever else. keeping on track with a larger project is a challenge for me. I often have to stop myself from bouncing to another file if an idea pops into my head.
Ok so I learned some self management, What else?
I have been dealing with typescript errors for about a year now. But on this project having to setup everything end to end. I Got a lot more familiar with debugging typescript
error.
To make my life a bit easier, I used zod
to manage the tRPC routes.
1getProjectsByCategory: publicProcedure
2 .input(
3 z.object({
4 category: z.string().min(3),
5 }),
6 )
7 .query(async ({ ctx, input }) => {
8 const project = await ctx.db.query.projects.findMany({
9 where: eq(projects.category, input.category.toUpperCase()),
10 orderBy: (projects, { desc }) => [desc(projects.createdAt)],
11 limit: 50,
12 with: { steps: true },
13 });
14 return project;
15 }),
And on the form side a controlled form element with zod
.
1const formSchema = z.object({
2 projectName: z.string().min(2).max(50),
3 category: z.string().min(2).max(50),
4 projectDescription: z
5 .string()
6 .min(10, { message: "Must be 10 or more characters long" })
7 .max(500, { message: "Must be less than 500 characters long" }),
8 projectImage: z
9 .instanceof(File)
10 .refine(
11 (file) => !ACCEPTED_IMAGE_TYPES.includes(file?.type),
12 "Only .jpg, .jpeg and .png formats are supported.",
13 )
14 .optional(),
15});
If I ever want to launch this live I figured it would be a good idea to limit abuse on any of the secured routes. The choice was pretty easy being as I work for a company that has a Ratelimit
skd.
1pnpm add @unkey/ratelimit
Following the docs is easy enough only a couple of steps.
created ratelimit
procedure
1export const rateLimitedProcedure = ({
2 limit,
3 duration,
4}: {
5 limit: number;
6 duration: number;
7}) =>
8 protectedProcedure.use(async (opts) => {
9 const unkey = new Ratelimit({
10 rootKey: env.UNKEY_ROOT_KEY,
11 namespace: `trpc_${opts.path}`,
12 limit: limit ?? 3,
13 duration: duration ? `${duration}s` : `${5}s`,
14 });
15
16 const ratelimit = await unkey.limit(opts.ctx.session.user.id);
17
18 if (!ratelimit.success) {
19 throw new TRPCError({
20 code: "TOO_MANY_REQUESTS",
21 message: JSON.stringify(ratelimit),
22 });
23 }
24
25 return opts.next({
26 ctx: {
27 ...opts.ctx,
28 remaining: ratelimit.remaining,
29 },
30 });
31 });
And then used like this on any route you want to ratelimit
1create: rateLimitedProcedure({ limit: 3, duration: 5 })
2 .input(
3 z.object({
4 projectName: z.string().min(3),
5 projectDescription: z.string(),
6 category: z.string(),
7 projectImage: z.string().optional(),
8 }),
9 )
This is probably what took the longest. My experience is limited with tRPC
routes and ratelimiting
. While I hate to bother people with my problems. Sometimes it is the best path forward when stuck one a problem.
In making this project I learned a hell of a lot. A more solid understanding of client/server communications. How to debug and fix Typescript
errors more effectively. Talk to someone who knows more than you. Docs will get you pretty far, but there is no substitute for another person to talk to. Big thanks to James and Andreasa for all the help over the last year. I would like to add more features to this in the future, but for now I have stuck the example into Unkey's example page for anyone interested.
2500 verifications and 100K successful rate‑limited requests per month. No CC required.