DEV Community

Cover image for Quick REST API File Upload with Hono JS and Drizzle
Aaron K Saunders
Aaron K Saunders

Posted on

Quick REST API File Upload with Hono JS and Drizzle

Overview

This is the second part of the series on REST API with Hono JS and Drizzle ORM. In the first part we setup the Hono server, connect a user's endpoint to Drizzle ORM that used SQLite as its database.

In this part we will show how to add the ability to upload images and then download the images for rendering in your application. We will add the additional API routes for list the files and managing the files.

Creating Upload API Endpoint

Basic Route Functionality

Create a new file in the routes directory called images-route.ts and add the following to get started.

import { Hono } from "hono";

const app = new Hono();

app.post("/", async (c) => {
  const body = await c.req.parseBody();
  const files = body.image;

  // check if files is an array and has length
  if (!files || (Array.isArray(files) && files.length === 0)) {
    return c.json({ message: "No files uploaded" }, 400);
  }

  // if files is not an array, convert it to an array
  const fileArray = Array.isArray(files) ? files : [files];


  return c.json({ fileArray });
});

export default app;
Enter fullscreen mode Exit fullscreen mode

This will return the array of files or throw error if invalid files are provided as a parameter to the endpoint.

Process Files, Get File Data

Next we need to process the array of files using promise.all; we will read the data into a buffer, since the plan is to save the data to the database from the buffer.

lets update the code with the basics for processing the files.

import { Hono } from "hono";

const app = new Hono();

app.post("/", async (c) => {
  const body = await c.req.parseBody();
  const files = body.image;

  // check if files is an array and has length
  if (!files || (Array.isArray(files) && files.length === 0)) {
    return c.json({ message: "No files uploaded" }, 400);
  }

  // if files is not an array, convert it to an array
  const fileArray = Array.isArray(files) ? files : [files];

  const processedFiles = await Promise.all(
    fileArray.map(async (file) => {
      if (!(file instanceof File)) {
        return c.json(
          {
            message: "Invalid file type",
            error: "Expected a file upload but received something else",
            received: typeof file,
          },
          400
        );
      }

      // load into a buffer for later use
      const buffer = Buffer.from(await file.arrayBuffer());

      return {
        name: file.name,
        size: file.size,
        type: file.type,
      };
    })
  );

  return c.json({ message: "Image uploaded", files: processedFiles }, 200);
});

export default app;
Enter fullscreen mode Exit fullscreen mode

so you can see we read the files, return metadata about the files. Output will be similar to this.

curl -X POST \
  http://localhost:3000/images-two \
  -H "Content-Type: multipart/form-data" \
  -F "image=@./profile63.jpeg"
{"message":"Image uploaded","files":[{"name":"profile63.jpeg","size":25892,"type":"image/jpeg"}]}                                    
Enter fullscreen mode Exit fullscreen mode

Create schema for Database Entry

Update the existing schema.ts file with following changes

// create images table
export const imagesTable = sqliteTable(
  "images_table",
  {
    id: int().primaryKey({ autoIncrement: true }),
    filename: text().notNull(),
    image_slug: text().notNull(),
    image_data: blob().notNull(),
    type: text().notNull(),
    size: int().notNull(),
    createdAt: text()
      .notNull()
      .default(sql`CURRENT_TIMESTAMP`),
    updatedAt: text()
      .notNull()
      .default(sql`CURRENT_TIMESTAMP`),
  },
  (table) => [
    uniqueIndex("filename_idx").on(table.filename),
    uniqueIndex("image_slug_idx").on(table.image_slug),
  ]
);

// export types for images table
export type Image = typeof imagesTable.$inferSelect;
export type InsertImage = typeof imagesTable.$inferInsert;
Enter fullscreen mode Exit fullscreen mode

nothing new here except we are using the blob object type to store the data we read into the buffer value in the api endpoint.

Now we have a place to save the meta-data and the file data.

Be sure to run the drizzle migration so the changes are pushed to the database

Update Endpoint to Save Data to the Database

Now that we have the schema in place, let's use Drizzle to save the file to the database.

First lets add in new imports needed

import { db } from "../db/index.js";
import { imagesTable } from "../db/schema.js";
Enter fullscreen mode Exit fullscreen mode

We need db to get the database and imagesTable is the schema object used in the query statement.

Now add this code inside your Promise.all loop to write the information to the database

  const processedFiles = await Promise.all(
    fileArray.map(async (file) => {
      if (!(file instanceof File)) {
        return c.json(
          {
            message: "Invalid file type",
            error: "Expected a file upload but received something else",
            received: typeof file,
          },
          400
        );
      }

      const buffer = Buffer.from(await file.arrayBuffer());

      await db.insert(imagesTable).values({
        filename: file.name,
        image_data: buffer,
        type: file.type,
        size: file.size,
      });

      return {
        name: file.name,
        size: file.size,
        type: file.type,
      };
    })
  );

  return c.json({ message: "Image uploaded", files: processedFiles }, 200);
});
Enter fullscreen mode Exit fullscreen mode

At this point you should be able to save data to the database using the endpoint.

Add GET Endpoint to Retrieve Images

Lets add a new endpoint to the file.

Note how we use Number to convert that parameter id, which is a string, to a number for the actual query.

// ADD TO IMPORTS
import { eq } from "drizzle-orm";

...

// get image by id
app.get("/:id", async (c) => {
  const id = c.req.param("id");
  const image = await db
    .select()
    .from(imagesTable)
    .where(eq(imagesTable.id, Number(id)));
  return c.json({ image });
});
Enter fullscreen mode Exit fullscreen mode

This will get all of the information from the database but might not be what you want when trying to render/download the actual image file.

Add this new API endpoint to download the image for rendering in the application user interface

// get image for download
app.get("/:id/download", async (c) => {
  const id = c.req.param("id");
  const image = await db
    .select()
    .from(imagesTable)
    .where(eq(imagesTable.id, Number(id)));

  if (!image || image.length === 0) {
    return c.json({ message: "Image not found" }, 404);
  }

  // set content type appropriately
  return c.body(image[0].image_data as Buffer, {
    headers: { "Content-Type": image[0].type },
    status: 200,
  });
});
Enter fullscreen mode Exit fullscreen mode

Examples of How to Use the API Endpoint

Using CURL

Uploading the image to the server using the following command

 curl -X POST \        
  http://localhost:3000/images \
  -H "Content-Type: multipart/form-data" \
  -F "image=@./profile63.jpeg"   
Enter fullscreen mode Exit fullscreen mode

will provide this response on success

{ 
  "message" : "Image uploaded",
  "files: [
     {
        "name":"profile63.jpeg",
        "size":25892,
        "type":"image/jpeg"
     }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Using Javascript in HTML Form

<form action="http://localhost:3000/images" method="post" enctype="multipart/form-data">
  <input type="file" name="image" multiple accept="image/*">
  <button type="submit">Upload</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Using React

function MultiImageUpload() {
  const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files?.length) return;

    const formData = new FormData();
    Array.from(e.target.files).forEach((file) => {
      formData.append('image', file);
    });

    try {
      const response = await fetch('http://localhost:3000/images', {
        method: 'POST',
        body: formData,
      });
      const data = await response.json();
      console.log('Upload success:', data);
    } catch (error) {
      console.error('Upload failed:', error);
    }
  };

  return (
    <input 
      type="file" 
      multiple  // Enable multiple file selection
      accept="image/*"
      onChange={handleUpload}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Source Code

Video

In the video there is additional content added, so please review the video for more information on uploading files, create REST APIs and integration with Drizzle ORM

  • limiting the file size of the upload
  • creating an image_slug as an unique identifer for each image
  • listing all images with meta-data only

Top comments (0)