Send, Store, and Show Images With React, Express and MongoDB
Sometimes it makes the most sense to store whole images in a database — improved security, redundancy, and centralization are some benefits of doing so.
During my most recent personal project I decided to store images like this and was surprised at the lack of information on how to do this using a React, Express, and MongoDB stack. I wrote this article to fill in the information I was unable to easily find, for those cases when it is the right decision to store images in a database.
It is worth mentioning that the approach demonstrated here — using MongoDB Buffer
— will only work with images smaller than 16MB. Larger than that would require GridFS.
The task: send images from a Raspberry Pi to a server. After receiving them, save those images to a database, retrieve and display them on the client.
Sending Images from a Raspberry Pi to a Node/Express Server
Images can be sent using the Python requests library. Specifically using the files
argument of the requests.post
function. This is an easy and convenient way to get JPEG images from one web server to another.
Send image from Raspberry pi to NodeJS server.
// sendImage.pyimport requestsimg_path = './path/to/img'
url = 'http://www.example.com/api/img_data'files = {'file': ('image.jpg', open(img_path, 'rb'), 'image/jpeg')}
r = requests.post(url, files=files)
print(r.text)
Receiving the Images on the Server
Our Image is cruising towards our Node/Express server. We are going to use an express.Router
to handle the data when it arrives.
Initialize the router/Node/Express.
// server.jsconst express = require('express');
var app = express();
var router = express.Router();
This router answers the request, along with Multer. Multer is “Node.js middleware for handling multipart/form-data". In other words, it allows us to access the files
we sent with our post request. After our server receives the post request, and before our route function runs, Multer stores the image on our server, then attaches the new image file’s path to our Express request object.
Set up the Multer object, and the route which will handle the post request.
// server.js....
// Sets up where to store POST imagesconst storage = multer.diskStorage({
destination: function (req, res, cb) {
cb(null, 'uploads/')
}
});const multer = require('multer');
const upload = multer({ storage: storage });router.route('/img_data')
.post(upload.single('file'), function(req, res) {
The upload.single('file')
line tells Multer to process the incoming image data and store it in the local file system. Later we can then use the fs
package to read the file, and store it in MongoDB.
MongoDB Model
Images are represented as arrays of binary data, in MongoDB we store this type of data with the Buffer
SchemaType.
Define the MongoDB model.
// ImgModel.jsvar mongoose = require('mongoose')
var Schema = mongoose.Schema;var ImgSchema = new Schema({
img: { data: Buffer, contentType: String}
}, {
timestamps: true
});module.exports = mongoose.model('Img', ImgSchema);
The timestamps: true
line tells our database to automatically save the creation and update timestamp of each entry.
Save the Image to MongoDB
Now that we have a MongoDB model and the image from our post request, we can use the fs
package to get the image, make a new Img
instance, and save that instance to the db.
// server.jsconst fs = require('fs');....router.route('/img_data')
.post(upload.single('file'), function(req, res) {
var new_img = new Img;
new_img.img.data = fs.readFileSync(req.file.path)
new_img.img.contentType = 'image/jpeg';
new_img.save(); res.json({ message: 'New image added to the db!' });
})
At this point if you look at your database and see <Binary Data>
you are successful in saving an image to MongoDB.
Serve the Image from the Server
In order to serve the image we are going to add a .get
function onto our /img_data
route.
// server.jsrouter.route('/img_data')
.post(
....
}
.get(function(req, res) {
Img.findOne({}, 'img createdAt', function(err, img) {
if (err)
res.send(err); // console.log(img); res.contentType('json');
res.send(img);
}).sort({ createdAt: 'desc' });
});
In this route we sort with createdAt: ‘desc'
and use the Mongoose findOne method to retrieve the latest image. Returning the image to the client is then as easy as setting the contentType
to 'json'
and sending away exactly what was returned from the database.
If we uncommented that console.log(img)
line above in order to see what is returned from the database, this is what would be printed to the console:
// console output from console.log(img){ _id: 5b55edb31c942c00049195dc,
createdAt: 2018-07-23T15:01:07.013Z,
img:
{ contentType: 'image/jpeg',
data:
Binary {
_bsontype: 'Binary',
sub_type: 0,
position: 260547,
buffer: <Buffer ff d8 ff e0 00 10 4a 46 49 46 00 01 01 01 00 60 00 60 00 00 ff fe 00 3c 43 52 45 41 54 4f 52 3a 20 67 64 2d 6a 70 65 67 20 76 31 2e 30 20 28 75 73 69 ... > } } }
createdAt
is the time we saved this image to our database, it is automatically generated by MongoDB.
contentType
is whatever value you save it as, for me that was 'image/jpeg'
but it could also be 'image/png'
or something else, it’s up to you.
Our Buffer
datatype in our model has been stored as a BSON object. The important part for us is buffer: <Buffer ff d8 ff e0 00 .... >
— our image represented as an array of binary data (displayed above in hex). This buffer array will be important when we work on the client side.
Convert Image Data from Array format to String format - Client Side
If all you are interested in is getting this running you will need the following function in your React component, take it and move to the next section. If you’re also intersted in how it works, read on.
Our server is sending our image as an array of binary data. For a browser to understand and display this image the data needs to be in a different format, namely a Base64 string.
Function to convert array of binary to a Base64 string. Credit for this solution goes to this Stack Overflow answer.
// index.js....arrayBufferToBase64(buffer) {
var binary = '';
var bytes = [].slice.call(new Uint8Array(buffer)); bytes.forEach((b) => binary += String.fromCharCode(b)); return window.btoa(binary);
};....
We take the fancy array we created server side (buffer: <Buffer ff d8 ff e0 00 .... >
), convert all those hex numbers (ff d8 ff...
) to a new encoding, Base64 — an easy way to represent binary data as ASCII text, so we can use it in HTML. A little wordy but the concept is easy: MongoDB likes buffers, HTML likes strings.
The new image data looks something like this: iVBORw0KGgoAAAANSUhEU...
. We can let the browser know what this data represents by prepending the new string with 'data:image/jpeg;base64'
. And with this new data representing our image we can simply render the image in HTML like so: <img src='data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEU...'/>
.
With this explanation you have everything you need to solve the problem we set forth, but let’s see how one would finish it out in React.
Fetch the Image on the Client
There is a route waiting to serve us our image data, and everything on the server side is ready. The only thing left to do is fetch the image using the client and display it to the user. Although you can request the image from the server and display it however you want, here we’ll be using React to display it and the Fetch API to get it from the server.
Set up the React component.
// index.jsimport React, { Component } from 'react';class Image extends Component {
constructor(props) {
super(props); this.state = {
img: ''
};
};....
We have made a new Image
component, defined the constructor, and given it some img: {}
state. By putting the image data in state we will be able to render the rest of the page, while in the background asynchronously load in our image.
Make the request.
// index.js....componentDidMount() {
fetch('http://yourserver.com/api/img_data')
.then((res) => res.json())
.then((data) => {
// console.log(img) var base64Flag = 'data:image/jpeg;base64,';
var imageStr = this.arrayBufferToBase64(data.img.data.data); this.setState({
img: base64Flag + imageStr
)}
})
}....
We put the fetch call in the componentDidMount()
function so only when the page has initially rendered do we begin our asynchronous image call.
If we uncommented the console.log(data)
line above we would see this in our browser’s console:
// browser console output from console.log(img){_id: "5b55...", createdAt: "2018-...", img: {...}}
createdAt: "2018-..."
img:
contentType: "image/jpeg"
data:
data: (260547) [255, 216, 255...]
type: "Buffer"
Looking above we see our precious image buffer can be accessed with data.img.data.data
(not the prettiest but it’l do). By passing this buffer into our arrayBufferToBase64
function we get the Base64 string we talked about before.
Setting the state
to img: base64Flag + imageStr
we cause the component to re-render with our ready made image data at our finger-tips.
Render the Component
Render function for our Image component.
// index.js....render() {
const {img} = this.state; return (
<img
src={img}
alt='Helpful alt text'/>
)
}export default Image;
If this is the first time our component is being rendered, const {img}
will be the empty string, since that is what we set it to in our constructor. On the second round (after the image has been loaded and converted), const {img}
will hold our new image string, causing our picture to show in the browser. Making it’s full round from Raspberry Pi, to Express server, through MongoDB, back through Express, finally achieving it’s life’s purpose with React.
Full Code
If you don’t want to read and piece together all the code above. Here’s everything you need to put this in your app.
// sendImage.pyimport requestsimg_path = './path/to/img'
url = 'http://www.yourwebserver.com/api/img'files = {'file': ('image.jpg', open(img_path, 'rb'), 'image/jpeg')}
r = requests.post(url, files=files)
print(r.text)// ImgModel.jsvar mongoose = require('mongoose')
var Schema = mongoose.Schema;var ImgSchema = new Schema({
img: { data: Buffer, contentType: String}
}, {
timestamps: true
});module.exports = mongoose.model('Img', ImgSchema);// server.jsconst express = require('express');
const fs = require('fs');var app = express();
var router = express.Router();const storage = multer.diskStorage({
destination: function (req, res, cb) {
cb(null, 'uploads/')
}
});const multer = require('multer');
const upload = multer({ storage: storage });router.route('/img_data')
.post(upload.single('file'), function(req, res) {
var new_img = new Img;
new_img.img.data = fs.readFileSync(req.file.path)
new_img.img.contentType = 'image/jpeg'; // or 'image/png'
new_img.save();res.json({ message: 'New image added to the db!' });
}).get(function(req, res) {
Img.findOne({}, 'img createdAt', function(err, img) {
if (err)
res.send(err); res.contentType('json');
res.send(img);
}).sort({ createdAt: 'desc' });
});
// index.jsimport React, { Component } from 'react';class Image extends Component {
constructor(props) {
super(props); this.state = {
img: ''
};
}; arrayBufferToBase64(buffer) {
var binary = '';
var bytes = [].slice.call(new Uint8Array(buffer)); bytes.forEach((b) => binary += String.fromCharCode(b)); return window.btoa(binary);
}; componentDidMount() {
fetch('http://yourserver.com/api/img_data')
.then((res) => res.json())
.then((data) => {
var base64Flag = 'data:image/jpeg;base64,';
var imageStr =
this.arrayBufferToBase64(data.img.data.data); this.setState({
img: base64Flag + imageStr
)}
})
} render() {
const {img} = this.state; return (
<img
src={img}
alt='Helpful alt text'/>
)
}export default Image;