Build a Simple Image Resizing API in less than 100 LOC
In this tutorial, we’ll build a simple image resizing API using ImageMagick
and deploy it on Deno Deploy. The API will resize images by stretching or
cropping them, although you can add more functionality later.
Follow the video tutorial here.
View the source, try the demo, or check out the playground.
ImageMagick
We’ll use ImageMagick
for all of the
image manipulation in our API. It’s one of the most popular libraries for
manipulating images, including resizing, format conversion, compression, and
effect application. More specifically, we’ll use
imagemagick_deno
, which is
ImageMagick
compiled to WebAssembly and compatible with Deno and Deno Deploy.
Setting Up The Server
First, create a directory for your project. Then, create a new TypeScript file
in that folder named main.ts
.
In main.ts
, let’s create a basic web server using
std lib’s http
module.
import { serve } from "https://deno.land/std@0.173.0/http/server.ts";
serve(
async (req: Request) => {
return new Response("Hello World!");
},
);
Now, you can run this script by opening a terminal in this directory and running:
deno run --allow-net main.ts
Now, navigate to localhost:8000
. You should see Hello World!
.
Manipulating Images
The next step is to add image manipulation. In main.ts
, let’s import a few
things from imagemagick_deno
:
import {
ImageMagick,
initializeImageMagick,
MagickGeometry,
} from "https://deno.land/x/imagemagick_deno@0.0.14/mod.ts";
Then, let’s initialize ImageMagick
, which sets the necessary configuration for
the binary and for its API to work.
await initializeImageMagick();
Next, we’ll confirm the necessary parameters exist and make sense, or we’ll
return a 400
(Bad Request) error code. We can access the querystring params
with the searchParams
function.
Let’s create a new function parseParams
, which will:
- check for necessary params
image
,height
, andwidth
- make sure that
height
andwidth
are greater than 0 and less than 2048 - return a
string
on error
function parseParams(reqUrl: URL) {
const image = reqUrl.searchParams.get("image");
if (image == null) {
return "Missing 'image' query parameter.";
}
const height = Number(reqUrl.searchParams.get("height")) || 0;
const width = Number(reqUrl.searchParams.get("width")) || 0;
if (height === 0 && width === 0) {
return "Missing non-zero 'height' or 'width' query parameter.";
}
if (height < 0 || width < 0) {
return "Negative height or width is not supported.";
}
const maxDimension = 2048;
if (height > maxDimension || width > maxDimension) {
return `Width and height cannot exceed ${maxDimension}.`;
}
return {
image,
height,
width,
};
}
In our serve()
function, we can access the querystring params and call
parseParams
:
// ...
serve(
async (req) => {
const reqURL = new URL(req.url);
const params = parseParams(reqURL);
if (typeof params === "string") {
return new Response(params, { status: 400 });
}
},
);
Now that we have ensured that the necessary parameters exist, let’s get the
image. Let’s create a new function getRemoteImage
, which will fetch
the
image by URL, check that the mediaType
is correct, and return a buffer
and
mediaType
(or an error message). Note, we will need to import
parseMediaType
at the top of the file.
import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts";
async function getRemoteImage(image: string) {
const sourceRes = await fetch(image);
if (!sourceRes.ok) {
return "Error retrieving image from URL.";
}
const mediaType = parseMediaType(sourceRes.headers.get("Content-Type")!)[0];
if (mediaType.split("/")[0] !== "image") {
return "URL is not image type.";
}
return {
buffer: new Uint8Array(await sourceRes.arrayBuffer()),
mediaType,
};
}
Then, in our serve()
function, after parseParams
we can call
getRemoteImage
and handle any errors:
// ...
serve(
async (req) => {
const reqURL = new URL(req.url);
const params = parseParams(reqURL);
if (typeof params === "string") {
return new Response(params, { status: 400 });
}
const remoteImage = await getRemoteImage(params.image);
if (remoteImage === "string") {
return new Response(remoteImage, { status: 400 });
}
},
);
Next, we can finally modify the image with ImageMagick
. Let’s create a new
function called modifyImage
, which will accept imageBuffer
and the params
object.
In this function, we’ll use the MagickGeometry
constructor to set the
parameters for the resizing. Additionally, to allow for adaptable resizing, if
either height
or width
is missing, we’ll set ignoreAspectRatio
to false
.
Finally, this function will return a Promise
that will resolve to the
transformed image as a Uint8Array
buffer:
function modifyImage(
imageBuffer: Uint8Array,
params: { width: number; height: number; mode: string },
) {
const sizingData = new MagickGeometry(
params.width,
params.height,
);
sizingData.ignoreAspectRatio = params.height > 0 && params.width > 0;
return new Promise<Uint8Array>((resolve) => {
ImageMagick.read(imageBuffer, (image) => {
image.resize(sizingData);
image.write((data) => resolve(data));
});
});
}
In serve()
, after we call getRemoteImage
, let’s add a call to modifyImage
.
Then, we can return a new Response
, which contains the modifiedImage
:
// ...
serve(
async (req: Request) => {
const reqURL = new URL(req.url);
const params = parseParams(reqURL);
if (typeof params === "string") {
return new Response(params, { status: 400 });
}
const remoteImage = await getRemoteImage(params.image);
if (remoteImage === "string") {
return new Response(remoteImage, { status: 400 });
}
const modifiedImage = await modifyImage(remoteImage.buffer, params);
return new Response(modifiedImage, {
headers: {
"Content-Type": remoteImage.mediaType,
},
});
},
);
Congratulations! You have created an API for resizing images.
Next, let’s add more flexibility in the API and allow cropping.
Cropping Images
Standard resizing works in most cases, but there are some cases where you might want to crop images instead. Cropping will allow you to avoid squishing the image if you change the aspect ratio and remove unnecessary areas in the image.
In the parseParams
function, let’s check for accepted modes (“crop” and
“resize”), as well as add mode
to the return object:
function parseParams(reqUrl: URL) {
const image = reqUrl.searchParams.get("image");
if (image == null) {
return "Missing 'image' query parameter.";
}
const height = Number(reqUrl.searchParams.get("height")) || 0;
const width = Number(reqUrl.searchParams.get("width")) || 0;
if (height === 0 && width === 0) {
return "Missing non-zero 'height' or 'width' query parameter.";
}
if (height < 0 || width < 0) {
return "Negative height or width is not supported.";
}
const maxDimension = 2048;
if (height > maxDimension || width > maxDimension) {
return `Width and height cannot exceed ${maxDimension}.`;
}
const mode = reqUrl.searchParams.get("mode") || "resize";
if (mode !== "resize" && mode !== "crop") {
return "Mode not accepted: please use 'resize' or 'crop'.";
}
return {
image,
height,
width,
mode,
};
}
Then, in the modifyImage
function, if the mode is crop
, we’ll use
image.crop()
:
function modifyImage(
imageBuffer: Uint8Array,
params: { width: number; height: number; mode: "resize" | "crop" },
) {
const sizingData = new MagickGeometry(
params.width,
params.height,
);
sizingData.ignoreAspectRatio = params.height > 0 && params.width > 0;
return new Promise<Uint8Array>((resolve) => {
ImageMagick.read(imageBuffer, (image) => {
if (params.mode === "resize") {
image.resize(sizingData);
} else {
image.crop(sizingData);
}
image.write((data) => resolve(data));
});
});
}
That is it! Now, if you want to try your API, just run
deno run --allow-net main.ts
, as we did before, and access it using a URL
containing the width, height, image URL, and mode. For example, the URL
localhost:8000/?image=https://deno.land/images/artwork/deno_city.jpeg&width=500&height=500
should give you something like this:
You have a working image resizer!
Deploying to Deno Deploy
You can host your app at the edge with Deno Deploy, our multi-tenant JavaScript serverless cloud.
First, create a GitHub repo containing main.ts
. Yes, it can be a one file
repo. Then, go to https://deno.com/deploy and connect your GitHub account.
Create a new project from GitHub, and select the repo you just created. Select
“GitHub Automatic”, which will deploy every time there’s a merge onto your
main
branch. Finally, your entrypoint should be main.ts
.
Once you’ve connected, it should take up to a minute to deploy. Once it’s live, you can visit your URL.
What’s next?
Building an image resizing API with Deno and ImageMagick is not only simple and fast, but can be done within 100 lines of code. Not only that, but you can host this API globally at the edge, to minimize latency for your API’s consumers. You can extend this with more functionality like adding text or saving it into a storage service.
Got stuck or want to share what you’re working on? Come say hi on Discord or Twitter!