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;
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;
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"}]}
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;
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";
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);
});
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 });
});
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,
});
});
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"
will provide this response on success
{
"message" : "Image uploaded",
"files: [
{
"name":"profile63.jpeg",
"size":25892,
"type":"image/jpeg"
}
]
}
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>
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}
/>
);
}
Source Code
- See Branch File Upload
- https://github.com/aaronksaunders/hono-drizzle-node-app-1
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)