project banner

Building a Fantasy app

project banner

This project emerged out of passion and necessity. A friend had recently created a fantasy league but It was using google forms and google sheets. Although functional the user experience wasn't great.

My primary objectives were

Desired Community building features

Creating a team

The first problem I wanted to tackle was improving the UX of the team creation process. Removing any uncertainties about what impact your choices have on points, and bonus points throughout the tournament.


One key issue I had when deciding on players was knowing if they were actually good. In the original there was a separate spreadsheet showcasing all stats for all players. I personally found this slow and hard to use.

Instead, I went for a show on hover approach. If a user wanted to know the player stats they can simple hover over them.

Another quality-of-life feature added was sorting the players. A simple yet great way for users to find a player that fits in their budget. Without this it can be easy to miss a cheap player as your scroll through the teams.


Seeing how your players are performing is a key part of the user experience. As a user, you want feedback on your decisions.


When you open your team page, you are greeted with all of the players you selected. They also include your applied bonuses. I also opted for a simple point element for simplicity. If a user wants further details, all they need to do is scroll to see a more detailed points table.


Rate Limiting with Redis

After seeing a video about Upstash from the youtuber Theo I realised it would be good idea to add some basic rate limiting. After setting up the redis db on upstash I went ahead and added middleware to my project.

This tracks the number of requests each ip makes and if it hits the limit of requests it will forward the user /api/blocked. It required some testing to ensure a genuine user navigating the site doesn't hit the rate limit. I landed on 10 requests per second being the cap. Most pages only have 1-2 requests to the API at a time so if someone were to activate the rate limit there must have been a fair amount of spamming.

const ratelimit = new Ratelimit({
    redis: Redis.fromEnv(),
    limiter: Ratelimit.fixedWindow(10, "10 s"),
export default async function middleware(
 request: NextRequest,
 event: NextFetchEvent,
): Promise<Response | undefined> {
    const ip = request.ip ?? "";
    if (request.nextUrl.pathname === '/api/blocked') {
    const { success, pending, limit, reset, remaining } = await ratelimit.limit(
    const res = success
        : NextResponse.rewrite(new URL("/api/blocked", request.url), request);
    res.headers.set("X-RateLimit-Limit", limit.toString());
    res.headers.set("X-RateLimit-Remaining", remaining.toString());
    res.headers.set("X-RateLimit-Reset", reset.toString());
    return res;
export const config = {
matcher: "/api/:path*",

Admin dashboard

It was important to me that I did not have to be involved in running the tournaments themeselves. I set out to make a simple dashboard for verified users to access and manipulate league and player details.

The image below shows a page that admins can access to edit player details. I use an Amazon S3 bucket to store player images and use a service called "UploadThing" to manage upload and retrival.


It's important for an admin to be able to know the populaity of each player duing the tournement. We use this information to adjust prices as the event goes on in an attempt to create more variety in teams. I.E. If player Y is listed at $20,000 but has not been picked we will reduce his price by $1000


Building a profanity filter

Because users team names will be visible publicly on the home page of a league its important to make sure they are moderated in some way. The first solution I found was a profanity filter package on npm - bad-words.

If the string included a profanity, it would replace it with ***. This was ok as I would be able to detect if the name had a profanity before they submitted it and make them change it. The issue with this and every other package I find was the if the profanity was merged with other letters, it would not be detected. For example, profanityyyyyyyy would be deemed acceptable.

After a long search I decided to make my own instead. I got a list of all swear words/slurs from oxford dictionary and would run a regex comparison. Now as long as any word included a listed word it was marked as profanity.

note: This solution isn't perfect as there are words/names that are considered appropriate that include profanities. For example, "Dickens". But I decided that it would be better to have false positives than negatives.