I use SvelteKit with TypeScript and highly recommend you do this as well. We can type our data shapes in the backend and later reuse them in the frontend to safely access the data in the UI and make our lives easier in case of type changes.
I combined SvelteKit with SQLite as a database, as it is lightweight and easy to maintain and fast. You can read more about SvelteKit + SQLite in my previous post. As a node SQLite driver, I use better-sqlite3 as I like its synchronous API.
In this tutorial, we will store the images inside the SQLite database itself. This is totally fine for small or medium-sized apps, but not recommended for large apps. For large apps, you can use specific file storage services. For example, Motimize is an Open-Source image server that can compress and resize images and can be self-hosted.
The app I am working with has music data like artists, tracks, and albums. We will do a file upload for the album title image. You can check out the whole process of building this app in my YouTube playlist.
To allow users to upload an image, we create a form with a file input. We set the form enctype attribute to multipart/form-data to allow the browser to send the image as a binary file. Per default, only the filename would be transferred to the server handler. We also set the accept attribute to image/* to only allow images to be uploaded.
We add a change handler to the file input and call the function handleImageUpload. This function will get the image file from the event and create a temporary URL for it in the variable uploadedImage. We can use this URL as src attribute value for an img element to show the image to the user after he selected it.
Additionally, we can disable the form submit button until the user selected an image.
To process the form submit, we can create a handler in the +page.server.ts file. We can access the image from the formData object with the type File. This object stores metadata about the image like the filename, the mime type, the file size and the last modified date. We can also access the file contents as an ArrayBuffer.
To store the image, we first need to create a table in the database. The file itself will be stored in a BLOB column, and we add columns for each metadata value. This results in the following SQL statement:
Now we can create the function that saves the image in our database. First we convert the image to a buffer with the Buffer.from() function as we can store it in the BLOB column in this form. The SQL statement either inserts a new row, or updates an existing one, depending on if there is already an image for the album. We do that by using the on conflict clause.
To retrieve the image from the database, we create a new API endpoint. Aside from the albumId we pass the filename as a parameter. Theoretically, we can serve the image under any name, as we can find only with the albumId. I believe we should only serve it under its original name, so we will check that later in the database function.
It is important to set response headers so that the browser can show the image correctly. We set the Content-Type header to the mime type of the image, Content-Length to the file size, and Last-Modified to our saved last change date. In the end, we return the image as a Blob object.
Now we need to create the getAlbumImage function in the database.ts file. The query is basic as we just select all columns from the database. We just make sure the requested filename is the right one, so we only serve it under its original name. Afterward, we convert the image data to a Blob object that we can return as the response.
To load the image, we only need to add another image element to the album page. I extended the load function to also return the image filename. With that, can just call our new API endpoint to request the image.