You want a midjourney api node js integration because your app needs image generation without sending users into Discord. A single fetch() call gets you a demo; a usable integration needs an async job lifecycle, a webhook, retry rules, and server-side credential handling. Midjourney has no official API, so any implementation is a third-party automation layer with the same account and Terms of Service risks described in our guide to whether Midjourney has an API.

Disclaimer

Nextjourney is not affiliated with Midjourney or Discord. The code below shows the shape of a REST integration, not an official Midjourney SDK. Any wrapper or self-hosted platform operating through Discord accounts carries third-party platform risk.

Midjourney API Node.js integration: the request shape

Treat generation as an asynchronous job. Your Node.js route submits a prompt, receives a job id, and returns immediately. Do not keep the HTTP request open while Discord automation waits for a result.

// server.js
import express from "express";

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

const API_URL = process.env.IMAGE_API_URL;
const API_KEY = process.env.IMAGE_API_KEY;

app.post("/generate", async (req, res) => {
  const { prompt } = req.body;
  if (typeof prompt !== "string" || prompt.trim().length < 3) {
    return res.status(400).json({ error: "prompt must be a non-empty string" });
  }

  const upstream = await fetch(`${API_URL}/v1/generate`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ prompt: prompt.trim() })
  });

  if (!upstream.ok) {
    return res.status(upstream.status).json(await upstream.json());
  }

  const job = await upstream.json();
  return res.status(202).json({ jobId: job.jobId, status: "queued" });
});

app.listen(3000, () => console.log("Image API adapter on :3000"));

Node.js 18 or later includes the Fetch API, so this example needs no extra HTTP client. Keep the upstream URL and key in environment variables; never ship them to a browser bundle or mobile app.

Model the job lifecycle, not just the image URL

A generation request should move through explicit states: queued, running, completed, or failed. Your application can show progress, retry only transient failures, and reconcile jobs after a worker restart.

For a small integration, polling is enough:

export async function waitForImage(jobId, { signal } = {}) {
  for (let attempt = 0; attempt < 30; attempt++) {
    const response = await fetch(`${API_URL}/v1/jobs/${jobId}`, {
      headers: { "Authorization": `Bearer ${API_KEY}` },
      signal
    });

    if (response.status === 429) {
      await delay(5000);
      continue;
    }
    if (!response.ok) throw new Error(`Job lookup failed: ${response.status}`);

    const job = await response.json();
    if (job.status === "completed") return job.imageUrl;
    if (job.status === "failed") throw new Error(job.error || "Generation failed");

    await delay(2000);
  }

  throw new Error("Generation timed out");
}

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Polling inside a user-facing request is the wrong default for a busy product. Prefer a webhook: submit the job with a callback URL, store the job id in your database, and let the provider notify your backend when the image is ready. If your client is React rather than Node-only, the Midjourney API with React integration guide covers the browser-to-backend boundary and job-status hook.

app.post("/webhooks/image-generation", express.json(), (req, res) => {
  const event = req.body;
  // Verify the provider signature before updating your database.
  if (event.type === "job.completed") {
    // markJobComplete(event.jobId, event.imageUrl)
  }
  if (event.type === "job.failed") {
    // markJobFailed(event.jobId, event.error)
  }
  res.sendStatus(204);
});

A webhook handler must be idempotent. Providers retry webhook delivery; processing the same job.completed event twice should not create two billing records or two gallery entries.

Handle rate limits and upstream failures in Node.js

A useful Node.js client distinguishes errors instead of retrying everything. A 400 prompt rejection is usually permanent. A 429 needs backoff. A 503 may recover. A 401 means the API credential or underlying session needs attention.

export async function submitWithRetry(payload, maxAttempts = 4) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await fetch(`${API_URL}/v1/generate`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${API_KEY}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(payload)
    });

    if (response.status === 202) return response.json();
    if (response.status === 400 || response.status === 401 || response.status === 403) {
      throw new Error(`Permanent request failure: ${response.status}`);
    }

    if (response.status === 429 || response.status >= 500) {
      if (attempt === maxAttempts) break;
      const retryAfter = Number(response.headers.get("retry-after"));
      const waitMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : Math.min(30000, 1000 * 2 ** (attempt - 1));
      await delay(waitMs);
      continue;
    }

    throw new Error(`Unexpected API response: ${response.status}`);
  }
  throw new Error("Generation unavailable after retries");
}

The retry loop belongs in a worker or queue consumer, not in every web request. Once volume rises, put requests into a queue and enforce concurrency per account. That is the same queue, worker, and account-rotation pattern explained in how to automate Midjourney at scale.

Secure your Midjourney API JavaScript client

The JavaScript SDK or service wrapper should run on your server. A browser client can call your own /generate route, but it should never know the upstream key. Add request authentication, input limits, and a per-user quota before forwarding prompts.

Store only what you need: your internal job id, upstream job id, status, timestamps, and final asset URL. If the upstream platform exposes account sessions, treat them like production secrets. Encrypt them at rest, restrict access to workers, and make rotation a deliberate admin operation.

The companion guide to Midjourney API authentication covers scoped keys, bearer headers, and safe rotation in more detail.

This security boundary also makes it easier to switch providers. Your product depends on your own small interface, while the Discord-facing implementation remains replaceable. That is a better design than scattering wrapper-specific calls across React components and background scripts.

Ready to self-host?

Full source code — backend, frontend, REST API, admin dashboard. One-time license from $399, no recurring platform fees.

See pricing

Ship the adapter, then own the queue

A Node.js integration proves the request path. Production reliability comes from the layers around it: async jobs, idempotent webhooks, bounded retries, and account-aware workers. If you need those pieces under your control, compare the self-hosted deployment path before committing to a wrapper. And when the adapter is ready, the frontend integration guide shows how to wire generation into an actual website without leaking credentials. Prefer Python? The Midjourney API Python integration tutorial walks through the same job lifecycle with sync and async examples.