Upload an image with TypeScript by reading the file, adding a typed Blob to FormData as file, and calling fetch. Parse JSON as unknown. Use type guards before you read fields. Accept HTTP 202 for new bytes and HTTP 200 for deduplicated bytes.
Before you upload
imgd.dev costs $1 per GB as a one-time storage purchase. It has no free tier. Every imgd.dev image is public to anyone with its URL. Do not upload a private, confidential, or secret image.
Use Node.js 20 or later and TypeScript 5.8 or later. Load IMGD_KEY from a secret manager.
Confirm that the environment has the key without printing it:
test -n "$IMGD_KEY"
Complete TypeScript example
Save this code as examples/core/typescript/upload-image.ts.
/// <reference path="./node-runtime.d.ts" />
import { readFile } from "node:fs/promises";
import { basename, extname } from "node:path";
const UPLOAD_URL = "https://imgd.dev/v1/upload";
const METADATA_URL = "https://imgd.dev/v1/images";
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
const REQUEST_TIMEOUT_MS = 60_000;
const MIME_TYPES: Readonly<Record<string, string>> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
};
type UploadStatus = "processing" | "live" | "review" | "blocked" | "error";
type UploadSuccess = {
hash: string;
url: string;
status: UploadStatus;
mime: string;
bytes: number;
width: number | null;
height: number | null;
alt_text: string | null;
unpublish_at: string | null;
deduplicated?: boolean;
note: string;
};
type ApiError = {
error: string;
fix: string;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isNullableNumber(value: unknown): value is number | null {
return value === null || typeof value === "number";
}
function isNullableString(value: unknown): value is string | null {
return value === null || typeof value === "string";
}
function isUploadStatus(value: unknown): value is UploadStatus {
return value === "processing"
|| value === "live"
|| value === "review"
|| value === "blocked"
|| value === "error";
}
function isUploadSuccess(value: unknown): value is UploadSuccess {
return isRecord(value)
&& typeof value.hash === "string"
&& typeof value.url === "string"
&& isUploadStatus(value.status)
&& typeof value.mime === "string"
&& typeof value.bytes === "number"
&& isNullableNumber(value.width)
&& isNullableNumber(value.height)
&& isNullableString(value.alt_text)
&& isNullableString(value.unpublish_at)
&& (value.deduplicated === undefined || typeof value.deduplicated === "boolean")
&& typeof value.note === "string";
}
function isApiError(value: unknown): value is ApiError {
return isRecord(value)
&& typeof value.error === "string"
&& typeof value.fix === "string";
}
async function readJson(response: Response): Promise<unknown> {
const text = await response.text();
try {
return JSON.parse(text) as unknown;
} catch {
throw new Error(`The server returned invalid JSON with HTTP ${response.status}.`);
}
}
function responseError(response: Response, payload: unknown): Error {
const guidance: Readonly<Record<number, string>> = {
401: "Load a valid IMGD_KEY from the secret manager.",
402: "Buy storage or delete unused images, then retry.",
413: "Reduce the image below 20 MB, then retry.",
415: "Use JPEG, PNG, GIF, WebP, or AVIF with the correct media type.",
};
const errorCode = isApiError(payload) ? payload.error : "unknown_error";
const fix = isApiError(payload)
? payload.fix
: guidance[response.status] ?? "Read the response and correct the request.";
return new Error(`Request failed with HTTP ${response.status} (${errorCode}): ${fix}`);
}
async function uploadImage(imagePath: string, apiKey: string): Promise<UploadSuccess> {
const extension = extname(imagePath).toLowerCase();
const mediaType = MIME_TYPES[extension];
if (!mediaType) {
throw new Error("Use a JPEG, PNG, GIF, WebP, or AVIF file name.");
}
const bytes = await readFile(imagePath);
if (bytes.byteLength === 0) {
throw new Error("The image file is empty.");
}
if (bytes.byteLength > MAX_UPLOAD_BYTES) {
throw new Error("The image is larger than the 20 MB upload limit.");
}
const form = new FormData();
form.append("file", new Blob([bytes], { type: mediaType }), basename(imagePath));
const response = await fetch(UPLOAD_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
},
body: form,
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
const payload = await readJson(response);
if (!response.ok) {
throw responseError(response, payload);
}
if (response.status !== 200 && response.status !== 202) {
throw new Error(`The upload returned unexpected HTTP ${response.status}.`);
}
if (!isUploadSuccess(payload)) {
throw new Error("The upload response does not match the expected success contract.");
}
return payload;
}
async function verifyUpload(upload: UploadSuccess, apiKey: string): Promise<void> {
const metadataResponse = await fetch(`${METADATA_URL}/${upload.hash}`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
const metadata = await readJson(metadataResponse);
if (!metadataResponse.ok) {
throw responseError(metadataResponse, metadata);
}
if (!isRecord(metadata) || metadata.hash !== upload.hash) {
throw new Error("The metadata response did not confirm the upload hash.");
}
const publicResponse = await fetch(upload.url, {
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!publicResponse.ok) {
throw new Error(`The public URL returned HTTP ${publicResponse.status}.`);
}
const contentType = publicResponse.headers.get("content-type") ?? "";
if (!contentType.startsWith("image/")) {
throw new Error(`The public URL returned the unexpected type '${contentType}'.`);
}
const publicBytes = await publicResponse.arrayBuffer();
if (publicBytes.byteLength === 0) {
throw new Error("The public URL returned an empty image body.");
}
}
const imagePath = process.argv[2];
const apiKey = process.env.IMGD_KEY;
if (!imagePath) {
process.stderr.write("Usage: node upload-image.js IMAGE_PATH\n");
process.exitCode = 64;
} else if (!apiKey) {
process.stderr.write("Set IMGD_KEY in the environment.\n");
process.exitCode = 78;
} else {
try {
const upload = await uploadImage(imagePath, apiKey);
await verifyUpload(upload, apiKey);
process.stdout.write(`${JSON.stringify(upload, null, 2)}\n`);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
}
}
The repository has no Node.js type package. This small local declaration file covers only the APIs in the example.
Save it as examples/core/typescript/node-runtime.d.ts:
declare module "node:fs/promises" {
export function readFile(path: string): Promise<Uint8Array<ArrayBuffer>>;
}
declare module "node:path" {
export function basename(path: string): string;
export function extname(path: string): string;
}
declare const process: {
argv: string[];
env: Record<string, string | undefined>;
stderr: {
write(message: string): boolean;
};
stdout: {
write(message: string): boolean;
};
exitCode: number | undefined;
};
If your project already uses the official Node.js type package, remove the reference line and this local declaration file.
Check and run the example
Run the strict type check:
npx tsc \
--noEmit \
--strict \
--target ES2022 \
--module NodeNext \
--moduleResolution NodeNext \
examples/core/typescript/upload-image.ts
Compile to a temporary directory, then run the JavaScript output:
rm -rf .tmp-typescript
npx tsc \
--strict \
--target ES2022 \
--module NodeNext \
--moduleResolution NodeNext \
--outDir .tmp-typescript \
examples/core/typescript/upload-image.ts
node .tmp-typescript/upload-image.js ./image.png
rm -rf .tmp-typescript
The code uses no any type for the response contract. Both the success body and error body start as unknown.
Expected JSON
A new image can produce this output after metadata and public URL checks succeed:
{
"hash": "0000000000000000000000000000000000000000000000000000000000000000",
"url": "https://i.imgd.dev/i/0000000000000000000000000000000000000000000000000000000000000000",
"status": "processing",
"mime": "image/png",
"bytes": 51200,
"width": 1280,
"height": 720,
"alt_text": null,
"unpublish_at": null,
"note": "the url works immediately, serving a blurred placeholder until the moderation check finishes and it sharpens to the real image"
}
New bytes return HTTP 202. Identical bytes return HTTP 200 and can include deduplicated: true.
The public URL works immediately. It serves a blurred placeholder while the status is processing.
Why the type guards matter
JSON.parse cannot prove the server shape. The code converts the parse result to unknown, then checks each required property.
isUploadSuccess checks nullable dimensions, nullable alt text, terminal status values, and the optional deduplication flag.
isApiError requires both error and fix. The program surfaces the server fix value when a request fails.
The program does not use an unchecked type assertion for the success response. A malformed response stops before field use.
Verify the result
verifyUpload requests the owner metadata with the bearer key. It confirms that the returned hash matches the upload.
It then requests the public URL without authentication. It requires HTTP 200, an image/* content type, and a non-empty body.
During processing, that body is a blurred placeholder. A later metadata request can report live, review, blocked, or error.
Recover from API errors
The response handler uses the HTTP status, stable error code, and server fix text.
| Status | Cause | Action |
|---|---|---|
401 |
IMGD_KEY is absent or invalid. |
Load a valid key from the secret manager. Do not print it. |
402 |
The account is unpaid or lacks quota. | Buy storage or delete unused images. Then retry. |
413 |
The file exceeds 20 MB. | Resize or compress it below 20 MB. |
415 |
The media type is unsupported. | Use JPEG, PNG, GIF, WebP, or AVIF with the correct extension. |
Do not retry these responses without a change. You can retry a network timeout with the same bytes. The same bytes return the same hash and URL.
An API error has valid JSON:
{
"error": "invalid_api_key",
"fix": "this key is not recognized; create a new account with POST /v1/accounts"
}
Security rules
Keep IMGD_KEY in the process environment and outside version control. Never put it in a URL, source file, or normal output.
The program prints only the success response. The response contains a public URL and no key.
Inspect each image before upload. Public storage does not protect private text, customer records, or production values.
Decide if TypeScript fits
This example fits typed Node.js tools that need explicit response validation. It also fits callers that must surface actionable server errors.
It does not fit private images, access control, unsupported files, images above 20 MB, or free-tier needs. Read the service evaluation guide first.
Result
The example narrows unknown JSON into a typed success value, then verifies both metadata and the immediate public image response.
Compare it with the JavaScript upload guide or the Python requests guide.
Give an agent the integration procedure. A person can start from the home page.