<?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: Muhammad Aqib</title>
    <description>The latest articles on DEV Community by Muhammad Aqib (@mrpythonist).</description>
    <link>https://dev.to/mrpythonist</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%2F2569591%2F51f4dead-1c30-495a-9b70-38995adb39af.png</url>
      <title>DEV Community: Muhammad Aqib</title>
      <link>https://dev.to/mrpythonist</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrpythonist"/>
    <language>en</language>
    <item>
      <title>Stop Writing APIs Like It's 2015</title>
      <dc:creator>Muhammad Aqib</dc:creator>
      <pubDate>Sun, 28 Dec 2025 10:50:20 +0000</pubDate>
      <link>https://dev.to/mrpythonist/stop-writing-apis-like-its-2015-4ebc</link>
      <guid>https://dev.to/mrpythonist/stop-writing-apis-like-its-2015-4ebc</guid>
      <description>&lt;p&gt;We're in 2025, and I'm still seeing codebases treat APIs like they're just "endpoints that return JSON." If your API design hasn't evolved past basic CRUD routes, you're leaving performance, scalability, and developer experience on the table.&lt;br&gt;
Here's what needs to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stop Returning Everything by Default
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;// 2015 mindset&lt;br&gt;
app.get('/api/users/:id', async (req, res) =&amp;gt; {&lt;br&gt;
  const user = await db.users.findById(req.params.id);&lt;br&gt;
  res.json(user); // Returns 47 fields no one asked for&lt;br&gt;
});&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Why It's Bad:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kills mobile performance&lt;/li&gt;
&lt;li&gt;Exposes internal data structures&lt;/li&gt;
&lt;li&gt;Forces frontend to do filtering&lt;/li&gt;
&lt;li&gt;Makes caching nearly impossible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Modern Approach:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;app.get('/api/users/:id', async (req, res) =&amp;gt; {&lt;br&gt;
  const fields = req.query.fields?.split(',') || ['id', 'name', 'email'];&lt;br&gt;
  const user = await db.users.findById(req.params.id, { select: fields });&lt;br&gt;
  res.json(user);&lt;br&gt;
});&lt;/code&gt;&lt;br&gt;
Let the client request what it needs. GraphQL taught us this years ago—REST can do it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pagination Without Limits Is Criminal
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;app.get('/api/products', async (req, res) =&amp;gt; {&lt;br&gt;
  const products = await db.products.find(); // All 50,000 rows&lt;br&gt;
  res.json(products);&lt;br&gt;
});&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Why It's Bad:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One request can tank your database&lt;/li&gt;
&lt;li&gt;No way to handle growth&lt;/li&gt;
&lt;li&gt;Timeouts become user experienc
e&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Modern Approach:&lt;/strong&gt;&lt;br&gt;
`&lt;br&gt;
app.get('/api/products', async (req, res) =&amp;gt; {&lt;br&gt;
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);&lt;br&gt;
  const cursor = req.query.cursor;&lt;/p&gt;

&lt;p&gt;const products = await db.products.find({&lt;br&gt;
    where: cursor ? { id: { gt: cursor } } : {},&lt;br&gt;
    limit: limit + 1&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;const hasNext = products.length &amp;gt; limit;&lt;br&gt;
  const items = hasNext ? products.slice(0, -1) : products;&lt;/p&gt;

&lt;p&gt;res.json({&lt;br&gt;
    data: items,&lt;br&gt;
    cursor: hasNext ? items[items.length - 1].id : null&lt;br&gt;
  });&lt;br&gt;
});&lt;br&gt;
`&lt;br&gt;
Cursor-based pagination. Predictable load. Infinite scroll that doesn't destroy your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Error Responses Are Not an Afterthought
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;app.post('/api/orders', async (req, res) =&amp;gt; {&lt;br&gt;
  try {&lt;br&gt;
    const order = await createOrder(req.body);&lt;br&gt;
    res.json(order);&lt;br&gt;
  } catch (err) {&lt;br&gt;
    res.status(500).json({ error: 'Something went wrong' });&lt;br&gt;
  }&lt;br&gt;
});&lt;/code&gt;&lt;br&gt;
Frontend devs hate you for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modern Approach:&lt;/strong&gt;&lt;br&gt;
`app.post('/api/orders', async (req, res) =&amp;gt; {&lt;br&gt;
  try {&lt;br&gt;
    const order = await createOrder(req.body);&lt;br&gt;
    res.json({ data: order });&lt;br&gt;
  } catch (err) {&lt;br&gt;
    if (err.name === 'ValidationError') {&lt;br&gt;
      return res.status(400).json({&lt;br&gt;
        error: {&lt;br&gt;
          code: 'VALIDATION_FAILED',&lt;br&gt;
          message: 'Invalid order data',&lt;br&gt;
          fields: err.details&lt;br&gt;
        }&lt;br&gt;
      });&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (err.name === 'InsufficientStock') {
  return res.status(409).json({
    error: {
      code: 'INSUFFICIENT_STOCK',
      message: 'Product out of stock',
      productId: err.productId
    }
  });
}

// Log actual error server-side
logger.error(err);
res.status(500).json({
  error: {
    code: 'INTERNAL_ERROR',
    message: 'Failed to create order'
  }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
});`&lt;br&gt;
Structured errors. Actionable feedback. Frontend can actually handle this.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Rate Limiting Isn't Optional Anymore
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem&lt;/strong&gt;:&lt;br&gt;
No rate limiting = open invitation for abuse, accidents, or runaway scripts that cost you money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modern Approach:&lt;/strong&gt;&lt;br&gt;
`import rateLimit from 'express-rate-limit';&lt;/p&gt;

&lt;p&gt;const limiter = rateLimit({&lt;br&gt;
  windowMs: 15 * 60 * 1000, // 15 minutes&lt;br&gt;
  max: 100,&lt;br&gt;
  standardHeaders: true,&lt;br&gt;
  legacyHeaders: false,&lt;br&gt;
  handler: (req, res) =&amp;gt; {&lt;br&gt;
    res.status(429).json({&lt;br&gt;
      error: {&lt;br&gt;
        code: 'RATE_LIMIT_EXCEEDED',&lt;br&gt;
        message: 'Too many requests',&lt;br&gt;
        retryAfter: req.rateLimit.resetTime&lt;br&gt;
      }&lt;br&gt;
    });&lt;br&gt;
  }&lt;br&gt;
});&lt;br&gt;
app.use('/api/', limiter);`&lt;br&gt;
Protect your infrastructure. It's 2025—this should be default.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Versioning From Day One
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;app.get('/api/users', ...); //&lt;/code&gt; What happens when this needs to change?&lt;br&gt;
&lt;strong&gt;Modern Approach:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;app.get('/api/v1/users', ...);&lt;/code&gt;&lt;br&gt;
When you need breaking changes:&lt;br&gt;
&lt;code&gt;app.get('/api/v2/users', ...); // New behavior&lt;br&gt;
app.get('/api/v1/users', ...); // Still works for old clients&lt;/code&gt;&lt;br&gt;
Versioning isn't premature optimization. It's respecting your users and your future self.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Stop Ignoring HTTP Status Codes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;res.status(200).json({ error: 'User not found' }); // WHY&lt;br&gt;
Modern Approach:&lt;br&gt;
200 — Success&lt;br&gt;
201 — Created&lt;br&gt;
400 — Bad request (client's fault)&lt;br&gt;
401 — Unauthorized&lt;br&gt;
403 — Forbidden&lt;br&gt;
404 — Not found&lt;br&gt;
409 — Conflict (duplicate, constraint violation)&lt;br&gt;
422 — Unprocessable entity (validation failed)&lt;br&gt;
429 — Rate limited&lt;br&gt;
500 — Server error (your fault)&lt;/code&gt;&lt;br&gt;
Use them correctly. Clients, monitoring tools, and caching layers depend on this.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Caching Headers Are Free Performance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
Every request hits your database, even for data that hasn't changed in weeks.&lt;br&gt;
&lt;strong&gt;Modern Approach:&lt;/strong&gt;&lt;br&gt;
`app.get('/api/products/:id', async (req, res) =&amp;gt; {&lt;br&gt;
  const product = await db.products.findById(req.params.id);&lt;/p&gt;

&lt;p&gt;res.set({&lt;br&gt;
    'Cache-Control': 'public, max-age=300', // 5 minutes&lt;br&gt;
    'ETag': generateETag(product)&lt;br&gt;
  });&lt;/p&gt;

&lt;p&gt;res.json(product);&lt;br&gt;
});`&lt;br&gt;
CDNs, browsers, and proxies will do the work for you. Let them.&lt;br&gt;
The Real Cost of Legacy API Patterns&lt;br&gt;
I've inherited codebases where:&lt;br&gt;
A &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single unoptimized endpoint cost $800/month in database reads&lt;/li&gt;
&lt;li&gt;Missing rate limits led to accidental DDoS from a bug in a mobile app&lt;/li&gt;
&lt;li&gt;Poor error handling meant every bug report was "something went wrong"
Modern API design isn't about being trendy. It's about building systems that scale, survive user mistakes, and don't require a full rewrite in two years.
&lt;strong&gt;What outdated API patterns are you still seeing in 2025? Drop them in the comments.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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