Node.js – API with SQLite Uploading Images

Table of Contents
Overview
This tutorial will break down an example of a step form built with the Material UI library. The form will have three sections for name, additional questions and images to upload.
Source Code
Download SourcePrerequisites
Create a folder for your project and go into that directory and run the following commands.
npm init
npm install express
Nodemon
Nodemon restarts the node.js application when changes are made.
Note: The example from GitHub does not have nodemon installed. It will be updated on 09/24/2021.
npm install nodemon
Update the “scripts” section of the “package.json” file with the following:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js"
},
To start the application use npm start instead of node app.js
SQLite Driver
SQLite driver to perform CRUD operations on a SQLite database file. The SQLite Transactions library is to add the functionality of wrapping multiple SQL statements as a transaction.
npm install sqlite3
npm install sqlite3-transactions
VSCode SQLite Plugin – Optional

After it is install hit Control + Shift + P then type sqlite

Choose usersdb.sqlite

Choose Show Table

Multer – Handling multipart/form-data
Multer is a node.js middleware for handling multipart/form-data
, which is primarily used for uploading files. It is written on top of busboy for maximum efficiency.
npm i multer
GUID Generator
UUID creates RFC4122 UUIDs used for generating unique filenames.
npm install uuid
CORS – Cross Origins Resource Sharing
DotEnv If you want to use the API with an external application
npm i cors
Node.js API
API Imports
Import the following libraries
- Express
- SQLite3
- FS – File Support
- CORS – Cross Origins Resource Sharing for external web apps
- Multer – Multi-Part Forms for the files
- UUID – GUID Generator for the file names
const express = require('express');
const app = express();
const port = 3003;
var fs = require('fs');
const cors = require('cors');
const multer = require('multer')
const { v4: uuidv4 } = require('uuid');
const { randomUUID } = require('crypto');
var sqlite3 = require("sqlite3"),
TransactionDatabase = require("sqlite3-transactions").TransactionDatabase;
SQLite Database
Using a constant to hold the name of the Database carsdb.sqlite. Create a new database instance into db. If the SQLite database does not exist, insert four cars and images. Finally put the db instance in module.exports.
var sqlite3 = require("sqlite3"),
TransactionDatabase = require("sqlite3-transactions").TransactionDatabase;
const DBSOURCE = "usersdb.sqlite";
var db = new TransactionDatabase(
new sqlite3.Database(DBSOURCE, (err) => {
if (err) {
// Cannot open database
console.error(err.message)
throw err
}
else {
// ** EXAMPLE **
// ** For a column with unique values **
// email TEXT UNIQUE,
// with CONSTRAINT email_unique UNIQUE (email)
db.run(`CREATE TABLE Users (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Username TEXT,
DateModified DATE,
DateCreated DATE
)`,
(err) => {
if (err) {
// Table already created
}else{
// Table just created, creating some rows
var insert = 'INSERT INTO Users (Username, DateCreated) VALUES (?,?)'
db.run(insert, ["lisa33", Date('now')])
db.run(insert, ["craig44", Date('now')])
db.run(insert, ["alan54", Date('now')])
db.run(insert, ["tracy65", Date('now')])
}
});
db.run(`CREATE TABLE UserImages (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
UserId INTEGER,
Mimetype TEXT,
Filename TEXT,
Size INTEGER,
DateModified DATE,
DateCreated DATE
)`,
(err) => {
if (err) {
// Table already created
}
});
}
})
);
module.exports = db
Enable CORS and Express URL encoded.
To allow all jut use cors() without anything inside the curly braces.
app.use(
express.urlencoded(),
cors({
origin: 'http://localhost:3000'
})
);
Upload a Single Image
The single file upload checks if the parent record exists to avoid uploading orphaned files. If the parent record exists then get the file from the images folder and moves and renames it with a GUID into a subfolder labeled with the Parent ID.

The code for the action.
const upload = multer({ dest: './images/' })
app.post('/api/upload-single-file', upload.single('files'), function async (req, res) {
var dir = `./images/${req.body.UserId}/`;
if (!fs.existsSync(dir)){
fs.mkdirSync(dir, { recursive: true });
}
var oldPath = `./images/${req.file.filename}`
var newPath = `./images/${req.body.UserId}/${req.file.filename}.jpg`;
fs.rename(oldPath, newPath, function (err) {
if (err) throw err
console.log('Successfully Moved File')
})
var data = {
UserId: req.body.UserId,
Name: req.file.filename,
Mimetype: req.file.mimetype,
Size: req.file.size,
DateCreated: Date('now')
}
var sql ='INSERT INTO UserImages (UserId, Filename, Mimetype, Size, DateCreated) VALUES (?,?,?,?,?)'
var params = [data.UserId, data.Name, data.Mimetype, data.Size, Date('now')]
db.run(sql, params, function (err, result) {
if (err){
res.status(400).json({"error": err.message})
return;
}
});
res.status(200).json(req.file)
});
Upload Multiple Images
Uploading multiple files takes a different approach from a single file upload. Instead of a file uploaded to the images folder then moved and renamed, the image binaries are in an array. The array contains the following data
File {
fieldname: 'files',
originalname: 'iphone.jpg',
encoding: '7bit',
mimetype: 'image/jpeg',
buffer: <Buffer ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01 00 01 00 00 ff db 00 43 00 06 04 05 06 05 04 06 06 05 06 07 07 06 08 0a 10 0a 0a 09 09 0a 14 0e 0f 0c ... 11511 more bytes>,
size: 11561
}
The Upload Multiple Files action creates a directory under the images folder with the parent ID as the subfolder. Then the action checks if the parent record exists to avoid uploading orphaned filed. If the parent record exits then loop through the files array and uses UUID() to generate unique filenames while saving the mime type, Size, and uses the Image Buffer to create the images using fs.
The code for the action.
var uploads = multer();
app.post('/api/upload-multiple-files', uploads.array('files', 3), function async (req, res) {
var file = req.files;
var fileCount = 0;
var isUserExists = true;
var sql = "SELECT * FROM Users WHERE Id = ?"
db.all(sql, req.body.UserId, (err, rows) => {
if (err) {
res.status(400).json({"error":err.message});
return;
}
console.log('rows ', rows);
isUserExists = (rows.length > 0)? true : false;
console.log('isUserExists ', isUserExists);
if(isUserExists) {
var dir = `./images/${req.body.UserId}/`;
file.forEach(element => {
if (!fs.existsSync(dir)){
fs.mkdirSync(dir, { recursive: true });
}
var newFileName = `${uuidv4()}.jpg`;
var newPath = `./images/${req.body.UserId}/${newFileName}`;
var imageBinary = element.buffer;
try {
fs.writeFile(newPath, imageBinary, 'base64', function(err){});
} catch (error) {
console.log(error);
}
var data = {
UserId: req.body.UserId,
Filename: newFileName,
Mimetype: element.mimetype,
Size: element.size,
DateCreated: Date('now')
}
var sql ='INSERT INTO UserImages (UserId, Filename, Mimetype, Size, DateCreated) VALUES (?,?,?,?,?)'
var params = [data.UserId, data.Filename, data.Mimetype, data.Size, Date('now')]
db.run(sql, params, function (err, result) {
if (err){
res.status(400).json({"error": err.message})
return;
}
});
fileCount++;
});
res.json({
message: `Successfully uploaded ${fileCount} files`
})
}
else {
res.json({
message: `Record does not exist`
})
}
});
Example of the subfolder and filenames.

Add a User Record
This is done with an insert statement for the Users table. Check for missing fields and then insert a record. Return the Success message and the new record ID as the Last ID.
app.post("/api/vehicle", async (req, res) => {
var errors=[]
if (!req.body.Make){
errors.push("Make is missing");
}
if (!req.body.Model){
errors.push("Model is missing");
}
if (errors.length){
res.status(400).json({"error":errors.join(",")});
return;
}
var data = {
Make: req.body.Make,
Model: req.body.Model,
DateCreated: Date('now')
}
var sql ='INSERT INTO Vehicles (Make, Model, DateCreated) VALUES (?,?,?)'
var params =[data.Make, data.Model, Date('now')]
db.run(sql, params, function (err, result) {
if (err){
res.status(400).json({"error": err.message})
return;
}
res.json({
"message": "success",
"data": data,
"id" : this.lastID
})
});
})
Update User Record
Using Patch send the updated Username. Check if Username exists and use the id parameter to patch the record with the new username and ModifiedDate.
app.patch("/api/user/:id", async (req, res) => {
var errors=[]
if (!req.body.Username){
errors.push("Username is missing");
}
// Just in case there are more fields missing
if (errors.length){
res.status(400).json({"error":errors.join(",")});
return;
}
var data = [req.body.Username, Date('now'), req.params.id];
let sql = `UPDATE Users SET
Username = ?,
DateModified = ?
WHERE Id = ?`;
await db.run(sql, data, function(err) {
if (err) {
return console.error(err.message);
}
console.log(`Row(s) updated: ${this.changes}`);
});
res.json({
message: "success",
id: req.params.id,
changes: this.changes
})
})
Delete Parent Record and Remove Images
This is usually considered a bad practice to blow away all a parent, child records, and the uploaded files, but it is sometimes necessary to have an all in one delete action.
In order to delete a profile, we need the vehicle ID to query the Vehicle Images table for the location and file names, remove the files, delete the image records, and finally delete the parent record. All of this needs to be wrapped in a transaction to not commit the changes just in case something goes wrong during the process.
Note: To do this correctly a check should be in place to make sure the parent record exists before trying to delete records. The front end shouldn’t allow for non-existing records to be deleted but it’s still a good step to take to prevent crashing the API.

Get the file names and use the vehicle ID for the subfolder path. Loop through the result set, delete the files, and then remove the directory.
app.delete("/api/car/:id", async (req, res, next) => {
db.beginTransaction(function(err, transaction) {
// SELECT IMAGES FOR THIS RECORD - DELETE THE FILES & SUB DIRECTORY
db.all('SELECT Name FROM VehicleImages WHERE VehicleId = ?', req.params.id, (err, rows) => {
if (err) {
res.status(400).json({"error":err.message});
return;
}
// DELETE THE FILES FROM THE QUERY DATA (rows)
rows.forEach(item => {
console.log(item.Name);
var filePath = `./images/${req.params.id}/${item.Name}`;
console.log('filePath', filePath);
try {
fs.unlinkSync(filePath)
//file removed
} catch(err) {
console.error(err)
}
})
// REMOVE THE SUB DIRECTORY
var dirPath = `./images/${req.params.id}`;
fs.rmdir(dirPath, function(err) {
if (err) {
throw err
} else {
console.log("Successfully removed the empty directory!")
}
})
});
// DELETE IMAGE RECORDS
db.run(
'DELETE FROM VehicleImages WHERE VehicleId = ?',
req.params.id,
function (err, result) {
if (err){
res.status(400).json({"error": res.message})
return;
}
});
// DELETE PARENT RECORD
db.run(
'DELETE FROM Vehicles WHERE id = ?',
req.params.id,
function (err, result) {
if (err){
res.status(400).json({"error": res.message})
return;
}
});
transaction.commit(function(err) {
if (err) return console.log("Sad panda :-( commit() failed.", err);
console.log("Happy panda :-) commit() was successful.");
});
});
})
Run it with Postman
Single file upload
http://localhost:3003/api/upload-single-file

Upload Multiple Files
http://localhost:3003/api/upload-multiple-files

Add New Parent Record
http://localhost:3003/api/user

Update Parent Record
http://localhost:3003/api/user

Delete Parent Record
http://localhost:3003/api/user
This will remove the files , subdirectory, images records, and the parent record.

You must be logged in to post a comment.