<?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 Ahmad</title>
    <description>The latest articles on DEV Community by Muhammad Ahmad (@muhammad_ahmad_f65ad7758d).</description>
    <link>https://dev.to/muhammad_ahmad_f65ad7758d</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%2F3942286%2Fdd8f44f0-21ab-49c6-a95f-3febbbfa2ae2.jpg</url>
      <title>DEV Community: Muhammad Ahmad</title>
      <link>https://dev.to/muhammad_ahmad_f65ad7758d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammad_ahmad_f65ad7758d"/>
    <language>en</language>
    <item>
      <title>How DropZap Handles Instagram and TikTok Downloads: A Technical Walkthrough</title>
      <dc:creator>Muhammad Ahmad</dc:creator>
      <pubDate>Wed, 20 May 2026 12:41:49 +0000</pubDate>
      <link>https://dev.to/muhammad_ahmad_f65ad7758d/how-dropzap-handles-instagram-and-tiktok-downloads-a-technical-walkthrough-4ime</link>
      <guid>https://dev.to/muhammad_ahmad_f65ad7758d/how-dropzap-handles-instagram-and-tiktok-downloads-a-technical-walkthrough-4ime</guid>
      <description>&lt;h1&gt;
  
  
  How DropZap Handles Instagram and TikTok Downloads: A Technical Walkthrough
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://www.dropzap.digital" rel="noopener noreferrer"&gt;DropZap&lt;/a&gt; is a free social media video downloader built with Next.js 14. It handles Instagram, TikTok, Twitter/X, Facebook, Reddit, Pinterest, and Threads — all from one interface. I want to walk through the technical approach behind the two most complex platforms: &lt;strong&gt;Instagram&lt;/strong&gt; and &lt;strong&gt;TikTok&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; Next.js 14 (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download engine:&lt;/strong&gt; &lt;code&gt;yt-dlp&lt;/code&gt; via Node child process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio merging:&lt;/strong&gt; &lt;code&gt;ffmpeg&lt;/code&gt; (for Reddit, and some formats)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI:&lt;/strong&gt; Tailwind CSS + shadcn/ui&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Render / Railway (Docker container)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Instagram Download Flow
&lt;/h2&gt;

&lt;p&gt;Instagram is complex because it serves three distinct content types that require different handling:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Reels and Video Posts
&lt;/h3&gt;

&lt;p&gt;The API route &lt;code&gt;/api/stream&lt;/code&gt; receives the Instagram post URL from the client. The server spawns &lt;code&gt;yt-dlp&lt;/code&gt; with the URL and &lt;code&gt;--format bestvideo+bestaudio/best&lt;/code&gt; flags.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified — actual route handles cookies + proxies&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yt-dlp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bestvideo+bestaudio/best&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--merge-output-format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// pipe to stdout&lt;/span&gt;
  &lt;span class="nx"&gt;instagramUrl&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The raw bytes are streamed back to the client via a &lt;code&gt;ReadableStream&lt;/code&gt;. This avoids writing temp files to disk on the server, keeping the container stateless.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Photos (Single Image Posts)
&lt;/h3&gt;

&lt;p&gt;For photo posts, &lt;code&gt;yt-dlp&lt;/code&gt; extracts the direct CDN URL of the JPEG. We then proxy the CDN response to the client with &lt;code&gt;Content-Disposition: attachment&lt;/code&gt; headers so the browser triggers a download instead of navigating to the image.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Carousels (Multi-Slide Posts)
&lt;/h3&gt;

&lt;p&gt;Carousels are the trickiest. A single Instagram carousel URL can contain 2–20 slides. &lt;code&gt;yt-dlp&lt;/code&gt; returns them as a playlist of individual entries.&lt;/p&gt;

&lt;p&gt;The server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runs &lt;code&gt;yt-dlp --dump-json&lt;/code&gt; to get the manifest of all slide URLs&lt;/li&gt;
&lt;li&gt;Fetches each slide in parallel using &lt;code&gt;Promise.all&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Packages them into a ZIP using the &lt;code&gt;archiver&lt;/code&gt; npm package&lt;/li&gt;
&lt;li&gt;Streams the ZIP to the client
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;archiver&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;archiver&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;archive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;archiver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;zlib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// pipe archive to response&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slideUrl&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slideUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`slide-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.jpg`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  TikTok Download Flow
&lt;/h2&gt;

&lt;p&gt;TikTok is more aggressive about blocking programmatic access than Instagram. The key challenges:&lt;/p&gt;

&lt;h3&gt;
  
  
  Watermark Removal
&lt;/h3&gt;

&lt;p&gt;TikTok serves two versions of each video:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A "watermarked" version (with the floating TikTok logo + username)&lt;/li&gt;
&lt;li&gt;A "no-watermark" version accessible via a different CDN path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;yt-dlp&lt;/code&gt; knows which endpoint to hit for the no-watermark version because it parses TikTok's internal API response, which includes a &lt;code&gt;play_addr_h264&lt;/code&gt; field pointing to the clean copy. This is the same mechanism that tools like ssstik and SnapTik use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limiting and Anti-Bot
&lt;/h3&gt;

&lt;p&gt;TikTok's API endpoints rotate and require specific request headers (&lt;code&gt;User-Agent&lt;/code&gt;, &lt;code&gt;Referer&lt;/code&gt;, and a session token derived from the device signature). &lt;code&gt;yt-dlp&lt;/code&gt; maintains an updater that tracks these header requirements and patches them when TikTok changes their scheme — which happens roughly every 2–4 weeks.&lt;/p&gt;

&lt;p&gt;This is why keeping &lt;code&gt;yt-dlp&lt;/code&gt; updated is critical. In the Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Always pull the latest yt-dlp binary at build time&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; yt-dlp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On production, a weekly cron job runs &lt;code&gt;yt-dlp --update-to nightly&lt;/code&gt; to stay ahead of TikTok API changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rate Limiting on the DropZap Side
&lt;/h2&gt;

&lt;p&gt;The app applies a server-side rate limiter (1 request per 5 seconds per IP) using an in-memory &lt;code&gt;Map&lt;/code&gt; to track timestamps. No Redis needed for the traffic levels a free tool receives — if this ever needed to scale horizontally, swapping to Redis would be a one-line change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/rate-limit.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5 second window&lt;/span&gt;
  &lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Code splitting:&lt;/strong&gt; Every downloader tab is loaded via &lt;code&gt;next/dynamic&lt;/code&gt; with &lt;code&gt;ssr: false&lt;/code&gt;. Only Instagram (the default tab) is statically bundled. This cut unused-JS budget by ~317 KiB in a PageSpeed audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming responses:&lt;/strong&gt; Video bytes are streamed from &lt;code&gt;yt-dlp&lt;/code&gt;'s stdout directly to the HTTP response. The server never writes a temp file to disk. This matters both for latency and for running on containers with limited ephemeral storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No client-side state persistence:&lt;/strong&gt; The download history feature was removed after user feedback that it confused people. &lt;code&gt;localStorage&lt;/code&gt; had a habit of showing stale "pending" entries. Simpler is better.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The live tool is at &lt;a href="https://www.dropzap.digital" rel="noopener noreferrer"&gt;dropzap.digital&lt;/a&gt;. Instagram is the default tab; TikTok is next. Paste any public post URL, hit Download.&lt;/p&gt;

&lt;p&gt;If you're building something similar and have questions about any specific part of the architecture — cookie handling, the archiver pipeline, the yt-dlp subprocess management — ask in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>api</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
