Skip to main content
Projects ● 4 min read

Cutest Pokémon

Stack
  • Next.js
  • TypeScript
  • Prisma
  • tRPC
Source code
marekh19/cutest-mon
View the app
cutest-mon.marek.work

The Project

I wanted to learn more about connecting Next.js apps to databases, so I built upon Theo’s excellent tRPC tutorial. The result was “Cutest Pokémon” - a simple voting app where users choose between two randomly selected Pokémon. While heavily based on the tutorial, I made some modifications to make it my own learning experience.

What I Actually Built

Core Functionality

The app randomly selects two Pokémon from the Pokedex and lets users vote on which is cuter. Votes are stored in a simple database schema:

// schema.prisma
model Vote {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  votedFor       Pokemon @relation(name: "votesFor", fields: [votedForId], references: [id])
  votedForId     Int
  votedAgainst   Pokemon @relation(name: "votesAgainst", fields: [votedAgainstId], references: [id])
  votedAgainstId Int
}
Voting page screenshot

Random Selection Logic

Basic random number generation with duplicate prevention:

export const getOptionsForVote = () => {
  const firstId = getRandomPokemon()
  const secondId = getRandomPokemon(firstId)
  return [first, second]
}

export const getRandomPokemon: (notThisOne?: number) => number = (notThisOne) => {
  const pokedexNumber = Math.floor(Math.random() * MAX_POKEDEX_ID) + 1

  if (pokedexNumber !== notThisOne) return pokedexNumber
  return getRandomPokemon(notThisOne)
}

tRPC API

Two simple endpoints - one to get a Pokémon pair, another to record votes:

export const appRouter = trpc
  .router()
  .query('get-pokemon-pair', {
    async resolve() {
      const [first, second] = getOptionsForVote()
      const bothPokemon = await prisma.pokemon.findMany({
        where: { id: { in: [first, second] } },
      })
      return { firstPokemon: bothPokemon[0], secondPokemon: bothPokemon[1] }
    },
  })
  .mutation('cast-vote', {
    input: z.object({
      votedFor: z.number(),
      votedAgainst: z.number(),
    }),
    async resolve({ input }) {
      return await prisma.vote.create({
        data: {
          votedAgainstId: input.votedAgainst,
          votedForId: input.votedFor,
        },
      })
    },
  })

What I Did Differently from the Tutorial

  1. Styled Components: Used CSS-in-JS for styling (the tutorial used Tailwind)
  2. Project Structure: Organized code into features rather than by file type

Key Learning Outcomes

Prisma Basics

  • Setting up database schemas and relationships
  • Basic CRUD operations with the Prisma client
  • Database migrations and seeding

tRPC Fundamentals

  • Creating type-safe API endpoints
  • Connecting frontend and backend with shared types
  • Basic query and mutation patterns

Next.js Features

  • API routes for backend functionality
  • Static generation for the results page
  • Basic routing between pages
Results page screenshot

The Reality Check

This was very much a beginner project. The code is straightforward, the features are basic, and there’s nothing particularly innovative here. But it served its purpose:

  • Hands-on experience with modern tools I’d only read about
  • Understanding the flow from database to frontend
  • Building confidence in working with full-stack TypeScript
  • Learning through modification rather than just copying

What I’d Do Differently Now

Looking back, I’d change quite a few things:

  • I don’t use CSS-in-JS anymore - I don’t think it’s the right approach, but that’s a topic for another article
  • Extract more logic into custom hooks instead of keeping it inside components
  • Use a proper feature-sliced project structure
  • Avoid the old pattern of naming everything index.ts inside nested folders (I cringe looking back 😅)
  • Split components more thoughtfully
  • Use constants instead of magic numbers/strings
  • Dependencies are outdated - upgrading would cause too many breaking changes, so not worth it here

On a practical note, I recently had to switch the database provider to Turso to keep the app running on a free tier. It was initially on PlanetScale, but they killed their free tier a while back, so I resurrected this project with Turso just to make it usable again.

Conclusion

“Cutest Pokémon” is exactly what it sounds like - a learning project that helped me understand the basics of modern full-stack development. It’s not impressive code, but it represents an important step in my journey. Sometimes the best way to learn is to build something simple, make it work, and then understand why it works.

The project taught me that tools like Prisma and tRPC make full-stack development much more approachable than I initially thought, even if the end result is just a basic voting app.