DEV Community

Cover image for Streamlining Image Moderation API Uploads in Node.js: From Native Fetch to Form‑Data and DNS Fixes ⚙
Ali nazari
Ali nazari

Posted on

Streamlining Image Moderation API Uploads in Node.js: From Native Fetch to Form‑Data and DNS Fixes ⚙

In this post, we’ll walk through our journey integrating the Sightengine image moderation API into a Node.js application. Along the way, we encountered and solved several challenges:

  1. Native fetch + form-data incompatibility
  2. DNS resolution failures in Node.js
  3. Blob vs. stream limitations
  4. Final streaming solution with form‑data + node‑fetch

1. The Initial Approach: Native fetch + form-data

We began by trying to mirror our working curl -F snippet in pure Node.js using the built‑in fetch (Undici) and the popular form-data package:

import FormData from 'form-data';
import fs from 'fs';
const data = new FormData();
data.append('media', fs.createReadStream('./image2.webp'));
  
await fetch('https://api.sightengine.com/1.0/check.json', {
  method: 'POST',
  headers: data.getHeaders(),
  body: data
});
Enter fullscreen mode Exit fullscreen mode

However, this led to a ConnectTimeoutError and eventually a DNS ENOTFOUND—even though curl worked perfectly.


2. Hunting Down DNS Failures

A quick nslookup api.sightengine.com returned:

;; Got SERVFAIL reply from 127.0.0.53
server can’t find api.sightengine.com: SERVFAIL
Enter fullscreen mode Exit fullscreen mode

Our system was using systemd-resolved on 127.0.0.53, which silently failed. The solution was to bypass this resolver and point to public DNS:

# /etc/systemd/resolved.conf
[Resolve]
DNS=8.8.8.8 1.1.1.1
FallbackDNS=1.1.1.1
Enter fullscreen mode Exit fullscreen mode

After sudo systemctl restart systemd-resolved, both nslookup and our Node.js code could resolve the API host.


3. Native FormData and Blob vs. Streams

Next, wanting to stick with pure native APIs, we tried using:

import { Blob } from 'buffer';
const stream = fs.createReadStream('image.jpg');
const blob = new Blob([stream], { type: 'image/jpeg' });
formData.append('media', blob, 'image.jpg');
Enter fullscreen mode Exit fullscreen mode

But Undici’s FormData.append still only accepts string, Buffer, or real Blob contents—not raw Node streams—so the API returned a “could not read image” error.


4. The Final, Battle‑Tested Solution

We ultimately opted for the most reliable approach in Node.js land:

  1. Install
   npm install form-data node-fetch
Enter fullscreen mode Exit fullscreen mode
  1. Code
   import fs from 'fs';
   import fetch from 'node-fetch';
   import FormData from 'form-data';

   const form = new FormData();
   form.append('media', fs.createReadStream('./image.jpg'));
   form.append('models', 'nudity-2.1,weapon,offensive-2.0,gore-2.0,self-harm');
   form.append('api_user', 'your-api-user');
   form.append('api_secret', 'your-api-secret');

   const res = await fetch('https://api.sightengine.com/1.0/check.json', {
     method: 'POST',
     body: form,
     headers: form.getHeaders()
   });
   const result = await res.json();
   console.log(result);
Enter fullscreen mode Exit fullscreen mode

This mirrors curl -F exactly, streams the file (no big memory hit), and produces valid multipart requests.


Conclusion

Integrating file uploads in Node.js can feel straightforward—until you hit native vs. library quirks and platform DNS oddities. By:

  1. Fixing your DNS resolver
  2. Recognizing Undici’s FormData limitations
  3. Falling back to form-data + node-fetch

you’ll have a rock‑solid, streaming multipart setup that behaves just like curl -F.

Top comments (10)

Collapse
 
deividas_strole profile image
Deividas Strole

Great breakdown! Switching from native fetch to form-data in Node.js can really smooth out multipart uploads, especially when dealing with image moderation APIs. And that sneaky DNS resolution issue? Classic Node headache — glad you tackled it head-on. These kinds of insights save devs a ton of debugging time!

Collapse
 
silentwatcher_95 profile image
Ali nazari

Thanks! 🙌 Yeah, switching to form-data made a huge difference

Collapse
 
abrar_ahmed profile image
Abrar ahmed

Great insights! I’ve faced similar issues with Undici’s FormData while managing file streams — it’s really frustrating when it fails without any notice during an API upload.
If you’re debugging this: make sure your multipart payload is actually streaming, and don’t underestimate the quirks of DNS resolvers on Linux (I’ve been caught out by 127.0.0.53 more times than I can count).
A good fallback is using node-fetch with form-data. I’m curious if you’ve tried axios too? It’s great with streams, but it does come with a bit of overhead.

Collapse
 
silentwatcher_95 profile image
Ali nazari

I’ve mostly stuck with node-fetch + form-data as a reliable combo, but haven’t seriously tried axios for stream uploads yet.

Collapse
 
nevodavid profile image
Nevo David

pretty cool seeing someone stick it out til stuff finally works - you think it’s patience or just stubbornness that actually makes you push through all the tech headaches?

Collapse
 
silentwatcher_95 profile image
Ali nazari

Honestly, probably a mix of both

Collapse
 
arshtechpro profile image
ArshTechPro

The DNS resolution issue via systemd-resolved was especially tricky—thanks for highlighting that fix. Also appreciate the clarity on Undici's Blob vs. stream limitations; it's not well-documented and can waste hours. The final solution with form-data and node-fetch is battle-tested and solid.

Collapse
 
silentwatcher_95 profile image
Ali nazari

Glad the fix helped!

Collapse
 
dotallio profile image
Dotallio

Super helpful breakdown - I’ve run into almost the exact same fetch vs. form-data headaches.
Did you see any signs that native APIs might eventually make this less painful?

Collapse
 
silentwatcher_95 profile image
Ali nazari

Appreciate it! Yeah, you're definitely not alone—handling fetch and form-data in Node can feel way rougher than it should be. As for native APIs, there's some movement: newer Node versions are gradually aligning more with web standards (like the experimental FormData and Blob in undici), but there’s still a lot of edge-case pain, especially with streaming.