<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex</title>
    <description>The latest articles on DEV Community by Alex (@ap_ufd).</description>
    <link>https://dev.to/ap_ufd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3826374%2Ff390576d-f100-45fc-828e-5a8c17711e30.png</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/ap_ufd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ap_ufd"/>
    <language>en</language>
    <item>
      <title>I built a Football Player Data API with Node.js (rate limiting, caching, filtering) — here’s what I learned</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Tue, 17 Mar 2026 18:59:36 +0000</pubDate>
      <link>https://dev.to/ap_ufd/i-built-a-football-player-data-api-with-nodejs-rate-limiting-caching-filtering-heres-what-i-118d</link>
      <guid>https://dev.to/ap_ufd/i-built-a-football-player-data-api-with-nodejs-rate-limiting-caching-filtering-heres-what-i-118d</guid>
      <description>&lt;p&gt;Over the past few months, I kept running into the same problem while building side projects.&lt;/p&gt;

&lt;p&gt;I like creating football related tools, things like squad builders, player comparison apps, and small analytics dashboards.&lt;/p&gt;

&lt;p&gt;But every time I started a new project, I hit the same wall: Getting clean, structured football player data is harder than it should be.&lt;/p&gt;

&lt;p&gt;Most of the available options are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scraped from websites&lt;/li&gt;
&lt;li&gt;inconsistent between sources&lt;/li&gt;
&lt;li&gt;outdated&lt;/li&gt;
&lt;li&gt;or just painful to work with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After dealing with this a few too many times, I decided to stop relying on external data and build my own API!&lt;/p&gt;

&lt;p&gt;The stack&lt;/p&gt;

&lt;p&gt;I kept things simple but production-oriented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js + express js&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;In-memory caching&lt;/li&gt;
&lt;li&gt;Manual rate limiting&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Structured relational data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What I built *&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A REST API that serves structured football player data (similar to Ultimate Team style datasets).&lt;/p&gt;

&lt;p&gt;Example endpoint:&lt;/p&gt;

&lt;p&gt;GET /ufd/2021/players&lt;/p&gt;

&lt;p&gt;Example response (simplified):&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "name": "Ronaldo",&lt;br&gt;
  "fullname": "C. Ronaldo dos Santos Aveiro",&lt;br&gt;
  "birth_date": "1985-02-05",&lt;br&gt;
  "overall_index": 99,&lt;br&gt;
  "position": "ST",&lt;br&gt;
  "foot": "Right",&lt;br&gt;
  "club": "Piemonte Calcio",&lt;br&gt;
  "league": "Serie A TIM",&lt;br&gt;
  "country": "Portugal",&lt;br&gt;
  "speed_indexOverallStat": 98,&lt;br&gt;
  "shooting_scoreOverallStat": 99,&lt;br&gt;
  "passing_indexOverallStat": 92,&lt;br&gt;
  "dribble_indexOverallStat": 99,&lt;br&gt;
  "defensive_indexOverallStat": 43,&lt;br&gt;
  "physical_indexOverallStat": 94,&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The goal make data easy to consume for developers building football apps.&lt;/p&gt;

&lt;p&gt;*** One challenge: rate limiting ***&lt;/p&gt;

&lt;p&gt;Since I wanted this to behave like a real API, I implemented a simple rate limiting system per API key.&lt;/p&gt;

&lt;p&gt;Here’s a simplified version:&lt;/p&gt;

&lt;p&gt;const rateLimitMap = new Map();&lt;br&gt;
const WINDOW = 60 * 1000;&lt;/p&gt;

&lt;p&gt;function checkRateLimit(apiKey, limit) {&lt;br&gt;
  const now = Date.now();&lt;br&gt;
  const user = rateLimitMap.get(apiKey) || { count: 0, start: now };&lt;/p&gt;

&lt;p&gt;if (now - user.start &amp;gt; WINDOW) {&lt;br&gt;
    user.count = 0;&lt;br&gt;
    user.start = now;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;user.count++;&lt;br&gt;
  rateLimitMap.set(apiKey, user);&lt;/p&gt;

&lt;p&gt;return user.count &amp;lt;= limit;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Not perfect, but good enough for a first version.&lt;/p&gt;

&lt;p&gt;*** Caching layer ***&lt;/p&gt;

&lt;p&gt;To avoid hitting the database on every request, I added a simple in-memory cache:&lt;/p&gt;

&lt;p&gt;const cache = new Map();&lt;br&gt;
const TTL = 60 * 60 * 1000;&lt;/p&gt;

&lt;p&gt;function getCache(key) {&lt;br&gt;
  const entry = cache.get(key);&lt;br&gt;
  if (!entry) return null;&lt;/p&gt;

&lt;p&gt;if (Date.now() &amp;gt; entry.expiry) {&lt;br&gt;
    cache.delete(key);&lt;br&gt;
    return null;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return entry.data;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This alone made a huge difference in performance.&lt;/p&gt;

&lt;p&gt;*** Filtering &amp;amp; search ***&lt;/p&gt;

&lt;p&gt;I also added a basic search system:&lt;/p&gt;

&lt;p&gt;LOWER(p.name) LIKE ?&lt;br&gt;
OR LOWER(p.fullname) LIKE ?&lt;/p&gt;

&lt;p&gt;And dynamically build queries depending on filters and pagination.&lt;/p&gt;

&lt;p&gt;*** Data modeling ***&lt;/p&gt;

&lt;p&gt;One of the most interesting parts was structuring the data.&lt;/p&gt;

&lt;p&gt;Instead of returning raw database rows, I grouped stats into categories:&lt;/p&gt;

&lt;p&gt;pace&lt;/p&gt;

&lt;p&gt;shooting&lt;/p&gt;

&lt;p&gt;passing&lt;/p&gt;

&lt;p&gt;dribbling&lt;/p&gt;

&lt;p&gt;defense&lt;/p&gt;

&lt;p&gt;physical&lt;/p&gt;

&lt;p&gt;This makes the API much easier to use on the frontend.&lt;/p&gt;

&lt;p&gt;*** What’s not perfect (yet) ***&lt;/p&gt;

&lt;p&gt;There are still things I want to improve:&lt;/p&gt;

&lt;p&gt;Move caching to Redis&lt;/p&gt;

&lt;p&gt;Improve query performance at scale&lt;/p&gt;

&lt;p&gt;Add better filtering (by league, club, rating, etc.)&lt;/p&gt;

&lt;p&gt;Clean up some stat naming inconsistencies&lt;/p&gt;

&lt;p&gt;*** What I learned ***&lt;/p&gt;

&lt;p&gt;A few takeaways from building this:&lt;/p&gt;

&lt;p&gt;You don’t need a complex stack to build something useful&lt;/p&gt;

&lt;p&gt;Data modeling matters more than you think&lt;/p&gt;

&lt;p&gt;Even simple caching can massively improve performance&lt;/p&gt;

&lt;p&gt;Building your own tools is sometimes faster than fighting bad ones&lt;/p&gt;

&lt;p&gt;*** Looking for feedback ***&lt;/p&gt;

&lt;p&gt;If you’ve built APIs before, I’d love your thoughts:&lt;/p&gt;

&lt;p&gt;Would you change how rate limiting is handled?&lt;/p&gt;

&lt;p&gt;Is in-memory caching a bad idea early on?&lt;/p&gt;

&lt;p&gt;Any suggestions for structuring this kind of data better?&lt;/p&gt;

&lt;p&gt;If you're building something similar, happy to share more details.&lt;/p&gt;

&lt;p&gt;***************** Project links *****************&lt;/p&gt;

&lt;p&gt;If you're interested in using it or exploring more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡Landing page: &lt;a href="https://ufdapi.netlify.app/" rel="noopener noreferrer"&gt;https://ufdapi.netlify.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐API on RapidAPI (free tier available): &lt;a href="https://rapidapi.com/ultimate-football-data-api-ultimate-football-data-api-default/api/ultimate-football-data-api" rel="noopener noreferrer"&gt;https://rapidapi.com/ultimate-football-data-api-ultimate-football-data-api-default/api/ultimate-football-data-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 GitHub repository: &lt;a href="https://github.com/alexpineda99/ultimate-football-data-api" rel="noopener noreferrer"&gt;https://github.com/alexpineda99/ultimate-football-data-api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm still actively improving it, so any feedback is really appreciated!&lt;/p&gt;

&lt;p&gt;If you’re building something football-related, I’d be happy to hear about it!&lt;/p&gt;

&lt;p&gt;Thanks for reading 🙌&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>node</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
