<?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: Abdullah Ahmed</title>
    <description>The latest articles on DEV Community by Abdullah Ahmed (@abdullah_ahmed_76eb910dac).</description>
    <link>https://dev.to/abdullah_ahmed_76eb910dac</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%2F3602554%2F6aac3ce3-8248-41c2-b3ff-bb5d6f4e4fe2.png</url>
      <title>DEV Community: Abdullah Ahmed</title>
      <link>https://dev.to/abdullah_ahmed_76eb910dac</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abdullah_ahmed_76eb910dac"/>
    <language>en</language>
    <item>
      <title>Data Normalization in Frontend Development: Simplifying Complex State Introduction</title>
      <dc:creator>Abdullah Ahmed</dc:creator>
      <pubDate>Sat, 08 Nov 2025 18:15:01 +0000</pubDate>
      <link>https://dev.to/abdullah_ahmed_76eb910dac/data-normalization-in-frontend-development-simplifying-complex-stateintroduction-2c0e</link>
      <guid>https://dev.to/abdullah_ahmed_76eb910dac/data-normalization-in-frontend-development-simplifying-complex-stateintroduction-2c0e</guid>
      <description>&lt;p&gt;When working on modern frontend applications, especially those with dynamic and interconnected data (like posts, comments, and users), managing state can quickly become complicated. APIs often return deeply nested data, and performing updates, deletions, or additions on such structures can feel like navigating a maze.&lt;/p&gt;

&lt;p&gt;For instance, imagine an app like Facebook or Reddit. Each post has comments, each comment has users, and each user might appear in multiple comments or posts. If you try to edit or delete a user, you’d have to find and update that user’s data in every place they appear. This approach quickly becomes unmanageable.&lt;/p&gt;

&lt;p&gt;That’s where data normalization comes in.&lt;/p&gt;

&lt;p&gt;What Is Data Normalization?&lt;/p&gt;

&lt;p&gt;Normalization means organizing your data so that each piece of information exists in exactly one place.&lt;br&gt;
Instead of keeping a deeply nested structure, you store data in flat, relational objects, similar to how relational databases work.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Instead of keeping users nested inside comments, which are nested inside posts,&lt;/p&gt;

&lt;p&gt;You separate them into different collections (posts, comments, users),&lt;/p&gt;

&lt;p&gt;Then you use IDs to link them together.&lt;/p&gt;

&lt;p&gt;This makes it much easier to update, delete, or add new data consistently.&lt;/p&gt;

&lt;p&gt;The Problem: Nested Data Example&lt;/p&gt;

&lt;p&gt;Let’s say our API returns this data for posts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const posts = [
  {
    id: 1,
    title: "Understanding React",
    comments: [
      {
        id: 101,
        text: "Great post!",
        user: {
          id: 1001,
          name: "Abdallah Ahmed",
        },
      },
      {
        id: 102,
        text: "Thanks for sharing",
        user: {
          id: 1002,
          name: "Sara Ali",
        },
      },
    ],
  },
];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, imagine you need to update the user’s name (for example, Abdallah changes his name).&lt;br&gt;
You’ll have to:&lt;/p&gt;

&lt;p&gt;Loop through each post,&lt;/p&gt;

&lt;p&gt;Then through each comment,&lt;/p&gt;

&lt;p&gt;Then find the user,&lt;/p&gt;

&lt;p&gt;And finally update the name.&lt;/p&gt;

&lt;p&gt;That’s a lot of unnecessary traversal — and if the same user appears in multiple posts or comments, you’ll need to update every occurrence manually.&lt;br&gt;
This leads to data duplication and inconsistent states.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution: Normalized Data Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After normalization, we can represent the same data like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const normalizedData = {
  posts: {
    1: { id: 1, title: "Understanding React", comments: [101, 102] },
  },
  comments: {
    101: { id: 101, text: "Great post!", user: 1001 },
    102: { id: 102, text: "Thanks for sharing", user: 1002 },
  },
  users: {
    1001: { id: 1001, name: "Abdallah Ahmed" },
    1002: { id: 1002, name: "Sara Ali" },
  },
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all entities (posts, comments, users) are stored separately and referenced by IDs.&lt;/p&gt;

&lt;p&gt;Example Operations&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updating a User’s Name&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Before normalization:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Update user's name (nested approach)
posts.forEach(post =&amp;gt; {
  post.comments.forEach(comment =&amp;gt; {
    if (comment.user.id === 1001) {
      comment.user.name = "Abdallah A.";
    }
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After normalization:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simple and efficient
normalizedData.users[1001].name = "Abdallah A.";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only one update is needed — and all parts of your app that reference this user will automatically get the updated name.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deleting a Comment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Before normalization:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Remove comment 101
posts[0].comments = posts[0].comments.filter(c =&amp;gt; c.id !== 101);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After normalization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Delete from comment list
delete normalizedData.comments[101];

// Remove reference from the post

normalizedData.posts[1].comments = normalizedData.posts[1].comments.filter(
  id =&amp;gt; id !== 101
);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, simple, and consistent — no deeply nested loops.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Adding a New Comment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Before normalization:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
const newComment = {
  id: 103,
  text: "Very helpful!",
  user: { id: 1003, name: "Ali Mohamed" },
};

posts[0].comments.push(newComment);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After normalization:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normalizedData.users[1003] = { id: 1003, name: "Ali Mohamed" };
normalizedData.comments[103] = { id: 103, text: "Very helpful!", user: 1003 };
normalizedData.posts[1].comments.push(103);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data remains structured, and each entity is tracked in one place.&lt;/p&gt;

&lt;p&gt;Benefits of Normalization&lt;/p&gt;

&lt;p&gt;✅ Easier updates – Change data in one place, not everywhere.&lt;br&gt;
✅ Less duplication – Each entity exists only once.&lt;br&gt;
✅ Simplified logic – Easier to add, remove, or merge entities.&lt;br&gt;
✅ Improved performance – Less re-rendering and simpler lookups in UI frameworks like React.&lt;br&gt;
✅ Scalable structure – Works great as your app and data grow in complexity.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>🚀 Building an Express.js API for the Amazon Scraper</title>
      <dc:creator>Abdullah Ahmed</dc:creator>
      <pubDate>Sat, 08 Nov 2025 14:31:44 +0000</pubDate>
      <link>https://dev.to/abdullah_ahmed_76eb910dac/building-an-expressjs-api-for-the-amazon-scraper-20eg</link>
      <guid>https://dev.to/abdullah_ahmed_76eb910dac/building-an-expressjs-api-for-the-amazon-scraper-20eg</guid>
      <description>&lt;p&gt;&lt;strong&gt;How I Finally Beat Amazon’s Bot Detection (and Built a Powerful Web Scraper That Works!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once your scraper function (scrapeAmazonProductPage) is ready, the next step is to wrap it inside a simple Express.js API.&lt;br&gt;
This allows you (or any client app) to send a request with a product URL and get structured data in return.&lt;/p&gt;

&lt;p&gt;📦 Step 1 — Install Dependencies&lt;/p&gt;

&lt;p&gt;If you haven’t already:&lt;/p&gt;

&lt;p&gt;npm install express puppeteer cheerio crawler&lt;/p&gt;

&lt;p&gt;You should now have these main dependencies:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{&lt;br&gt;
  "cheerio": "^1.0.0-rc.12",&lt;br&gt;
  "crawler": "^1.5.0",&lt;br&gt;
  "puppeteer": "^16.2.0",&lt;br&gt;
  "express": "^4.19.2"&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Make sure your Node.js version is 20 or above for optimal Puppeteer compatibility.&lt;/p&gt;

&lt;p&gt;🧱 Step 2 — Create Project Structure&lt;/p&gt;

&lt;p&gt;Here’s a suggested folder layout:&lt;/p&gt;

&lt;p&gt;amazon-scraper/&lt;br&gt;
├── package.json&lt;br&gt;
├── server.js&lt;br&gt;
└── src/&lt;br&gt;
    ├── scraper/&lt;br&gt;
    │   └── amazon.js&lt;br&gt;
    └── services/&lt;br&gt;
        └── scrapping.js&lt;/p&gt;

&lt;p&gt;server.js → entry point for Express&lt;/p&gt;

&lt;p&gt;src/scraper/amazon.js → your scraper logic (the code you already have)&lt;/p&gt;

&lt;p&gt;src/services/scrapping.js → optional, for error logging (you can mock this for now)&lt;/p&gt;

&lt;p&gt;🧠 Step 3 — Example Mock for Error Saver&lt;/p&gt;

&lt;p&gt;Create a dummy service in src/services/scrapping.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/services/scrapping.js
async function saveScrappingErrors(errorObj) {
    console.error("Scraping error:", errorObj);
}
module.exports = { saveScrappingErrors };

🧠 Step 4 — The Scraper (amazon.js)

Use your scraper function exactly as before.
Let’s slightly clean it for API use and export it properly:

// src/scraper/amazon.js
const puppeteer = require('puppeteer');
const cheerio = require('cheerio');
const Crawler = require('crawler');
const { saveScrappingErrors } = require('../services/scrapping');

const crawlPage = (url, browser) =&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; {
        const c = new Crawler({
            maxConnections: 100000,
            skipDuplicates: true,
            callback: async (error, res, done) =&amp;gt; {
                if (error) return reject(error);
                try {
                    const $ = cheerio.load(res.body);
                    if (!$('#histogramTable').length) return resolve(await crawlPage(url, browser));

                    const reviews = [];
                    const reviewElements = $('.a-section.review[data-hook="review"]');
                    const review_rating = $('[data-hook="average-star-rating"]').text().trim();
                    const review_count = $('[data-hook="total-review-count"]').text().trim().split(' ')[0];
                    const name = $('#productTitle').text().trim();
                    const description = $('#feature-bullets .a-list-item').text().trim();
                    const product_author = $('#bylineInfo').text().trim();

                    const regex = /\b\d+(\.\d+)?\b/;
                    reviewElements.each((_, el) =&amp;gt; {
                        const author = $(el).find('.a-profile-name').text().trim();
                        const content = $(el).find('.review-text').text().trim();
                        const title = $(el).find('[data-hook="review-title"]').text().trim();
                        const date = $(el).find('[data-hook="review-date"]').text().trim();
                        let stars = $(el).find('.review-rating span').text().trim();
                        const match = stars.match(regex);
                        stars = match ? parseFloat(match[0]) : '';
                        reviews.push({ author, content, title, date, rating: stars });
                    });

                    const extractStars = () =&amp;gt; {
                        const starsPercentageArray = [];
                        $('#histogramTable .a-histogram-row').each((_, el) =&amp;gt; {
                            const percentageText = $(el).find('.a-text-right a').text();
                            const percentage = parseInt(percentageText.replace('%', ''), 10);
                            const starsText = $(el).find('a.a-size-base').text();
                            const number_of_stars = parseInt(starsText, 10);
                            starsPercentageArray.push({ percentage: percentage || 0, number_of_stars });
                        });
                        return starsPercentageArray;
                    };

                    const extractMainImage = () =&amp;gt; $('#imgTagWrapperId img').attr('src') || '';

                    const core_price = $('#corePriceDisplay_desktop_feature_div .a-section .aok-offscreen').text().trim();
                    const currencyPattern = /\$\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?/;
                    const match = core_price.match(currencyPattern);
                    const extractedCurrency = match ? match[0] : "";

                    const extractImages = async () =&amp;gt; {
                        const htmlContent = res.body;
                        const page = await browser.newPage();
                        await page.setContent(htmlContent, { waitUntil: 'load', timeout: 0 });
                        const thumbnails = await page.$$('#altImages ul .imageThumbnail');
                        for (const thumbnail of thumbnails) {
                            await page.evaluate(el =&amp;gt; el instanceof HTMLElement &amp;amp;&amp;amp; el.scrollIntoView(), thumbnail);
                            await thumbnail.hover();
                        }
                        await page.waitForTimeout(1000);
                        const productData = await page.evaluate(() =&amp;gt; {
                            const images = [];
                            document.querySelectorAll('.a-unordered-list .image .imgTagWrapper img').forEach(img =&amp;gt; {
                                if (img &amp;amp;&amp;amp; img.src &amp;amp;&amp;amp; !img.src.endsWith('.svg')) images.push(img.src);
                            });
                            return images;
                        });
                        return productData;
                    };

                    const images_data = await extractImages();
                    resolve({
                        websiteName: 'Amazon',
                        reviews,
                        product_images_links: images_data,
                        review_rating,
                        review_count,
                        price: extractedCurrency,
                        name,
                        description,
                        product_author,
                        stars: extractStars(),
                        image_url: extractMainImage(),
                    });
                } catch (err) {
                    reject(err);
                } finally {
                    done();
                }
            },
        });
        c.queue(url);
    });
};

async function scrapeAmazonProductPage(homeUrl) {
    const browser = await puppeteer.launch({
        headless: true,
        ignoreHTTPSErrors: true,
        args: [
            "--disable-gpu",
            "--disable-dev-shm-usage",
            "--disable-setuid-sandbox",
            "--no-sandbox",
        ],
    });
    try {
        const data = await crawlPage(homeUrl, browser);
        return data;
    } catch (e) {
        await saveScrappingErrors({ error: e.message || e, url: homeUrl });
        return null;
    } finally {
        await browser.close();
    }
}

module.exports = { scrapeAmazonProductPage };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚡ Step 5 — Create Express API&lt;/p&gt;

&lt;p&gt;Now create server.js in the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// server.js
const express = require('express');
const cors = require('cors');
const { scrapeAmazonProductPage } = require('./src/scraper/amazon');

const app = express();
app.use(express.json());
app.use(cors());

// Health check
app.get('/', (req, res) =&amp;gt; {
    res.send('✅ Amazon Scraper API is running...');
});

// Main API endpoint
app.post('/api/scrape', async (req, res) =&amp;gt; {
    const { url } = req.body;

    if (!url || !url.includes('amazon')) {
        return res.status(400).json({ error: 'Invalid or missing Amazon URL' });
    }

    try {
        const data = await scrapeAmazonProductPage(url);
        if (!data) {
            return res.status(500).json({ error: 'Failed to scrape product data' });
        }
        res.json(data);
    } catch (error) {
        console.error('Scrape failed:', error);
        res.status(500).json({ error: error.message || 'Unexpected error' });
    }
});

const PORT = process.env.PORT || 4000;
app.listen(PORT, () =&amp;gt; console.log(`🚀 Server running on port ${PORT}`));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧪 Step 6 — Test the API&lt;/p&gt;

&lt;p&gt;Run the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use Postman, curl, or any HTTP client to test:&lt;/p&gt;

&lt;p&gt;Request:&lt;br&gt;
POST &lt;a href="http://localhost:4000/api/scrape" rel="noopener noreferrer"&gt;http://localhost:4000/api/scrape&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Content-Type: application/json&lt;br&gt;
{&lt;br&gt;
  "url": "&lt;a href="https://www.amazon.com/dp/B0BP9Z7K5V" rel="noopener noreferrer"&gt;https://www.amazon.com/dp/B0BP9Z7K5V&lt;/a&gt;"&lt;br&gt;
}&lt;br&gt;
Response:&lt;br&gt;
{&lt;br&gt;
  "websiteName": "Amazon",&lt;br&gt;
  "name": "Apple AirPods (3rd Generation)",&lt;br&gt;
  "price": "$169.99",&lt;br&gt;
  "review_rating": "4.7 out of 5 stars",&lt;br&gt;
  "review_count": "145,201",&lt;br&gt;
  "description": "Spatial Audio with dynamic head tracking...",&lt;br&gt;
  "product_author": "Apple",&lt;br&gt;
  "stars": [&lt;br&gt;
    { "number_of_stars": 5, "percentage": 85 },&lt;br&gt;
    { "number_of_stars": 4, "percentage": 10 }&lt;br&gt;
  ],&lt;br&gt;
  "product_images_links": [&lt;br&gt;
    "&lt;a href="https://m.media-amazon.com/images/I/61ZRU9gnbxL._AC_SL1500_.jpg" rel="noopener noreferrer"&gt;https://m.media-amazon.com/images/I/61ZRU9gnbxL._AC_SL1500_.jpg&lt;/a&gt;",&lt;br&gt;
    "&lt;a href="https://m.media-amazon.com/images/I/61dw1VHfwbL._AC_SL1500_.jpg" rel="noopener noreferrer"&gt;https://m.media-amazon.com/images/I/61dw1VHfwbL._AC_SL1500_.jpg&lt;/a&gt;"&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
⚙️ Step 7 — Tips for Production&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;✅ Use rate limiting to avoid Amazon blocking.&lt;br&gt;
✅ Deploy behind a proxy or rotating IP system if scraping frequently.&lt;br&gt;
✅ Consider puppeteer-extra-plugin-stealth for better evasion.&lt;br&gt;
✅ Cache results in a database if you’ll reuse them often.&lt;/p&gt;

</description>
      <category>node</category>
      <category>tutorial</category>
      <category>api</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
