Simple React image upload using GraphQL, PostgresQL, Sequelize, and Express

Mike Burton
8 min readAug 5, 2021

In this article I’m going to walk you through a recent coding challenge I was given for a senior front end position. We’ll cover creating a client in React using Create-React-App (CRA) that connects to an Express back end hosting a GraphQL API. We’ll also cover setting up Sequelize as the Object-Relational Mapping (ORM),while working with Postgres as our data store.

First off, for this project I’ve chosen to create this project as a monorepo. In this case that means that the server and the client files will all live in the same github repo, as shown in the screenshot below.

client and server folders in the monorepo

In order to get started we’ll first create a folder to house all of our code. The snippet below will help to get you started.

mkdir image-uploader
cd image-uploader
npx create-react-app client
mkdir server
cd server
npm init -y

We should now be in the server folder with a blank package.json repo. We can start off my adding some dependencies for express and graphql.

npm i -S express express-graphql graphql graphql-tools pg-hstore sequelize

For dev dependencies I like using nodemon for server updates on saves.

npm i -D nodemon

Before creating our graphql schema, we’ll want to establish a connection with our postgres db. This tutorial isn’t going to walk through setting up postgres locally, but if you’re on a mac running OSX you can likely use homebrew to accomplish this feat.

By default postgres will run on port 5432. We will use env variables to configure our connection URL to support deploying our repo down the road. Here is what my client file looks like.

const { Sequelize } = require("sequelize");
const { DEV_MODE, DB_URL } = require("../config");

// DB_URL = `postgres://localhost:5432/image-uploader`;
// note -> run "createdb image-uploader" from the CLI prior to connecting
const client = new Sequelize(DB_URL, {
dialect: "postgres",
dialectOptions: {
ssl: DEV_MODE ? false : true,
},
});
client
.authenticate()
.then(() => {
console.log("Connection has been established successfully.");
})
.catch((err) => {
console.error("Unable to connect to the database:", err);
});
module.exports = client;

Now that we have a db client we can create the model for an “Image” using Sequelize. I’ve done this as follows.

const { Model, Sequelize, DataTypes } = require("sequelize");
const client = require("../db");
class Image extends Model {}Image.init(
{
file: {
type: Sequelize.TEXT, // Allows for unlimited length of text
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
type: {
type: DataTypes.STRING,
},
},
{
sequelize: client,
modelName: "image",
}
);
Image.sync();module.exports = Image;

We can see that Image extends the sequelize Model, and that the init method accepts two objects as input parameters. The first will define the column names we’d like assigned to our table, while the second object contains the postgres client we created earlier and the modelName.

In order to create our graphql schema we’ll need to build out of our typeDefs and resolvers files. I prefer making these as folders inside of a parent graphql folder as below. I find this helps keep things modular as projects grow, but admittedly might be overkill for such a simple project (This is up to personal preference).

mkdir graphql
mkdir graphql/resolvers
touch graphql/resolvers/index.js
mkdir graphql/typeDefs
touch graphql/typeDefs/index.js

Starting off with our type definitions, or typeDefs file, we define our graphQL schema. For more information on types and schemas check out this link. Don’t worry if this isn’t all immediately clear to you, through more expose to graphql and its semantics you’ll gradually begin to grok the syntax. Note that these definitions will find there way into the graphiql playground, an incredibly useful tool for working with GraphQL APIs (Similar to Swagger docs).

const typeDefs = `
type Image {
file: String!
name: String!
type: String
}
type Query {
images: [Image]
search(terms: String!): [Image]
}
type Mutation {
addImage(file: String!, name: String!, type: String): Image
}
`;
module.exports = { typeDefs };

Now that we have our typeDefs and Image model written out, we can move on to our resolvers file. The complete snippet is available below.

const Image = require("../../models/image");
const { Op } = require("sequelize");
/**
Resolvers Map
Define's the technique for fetching the types defined in the schema.
The map below corresponds the the schema declarations in the typeDefs file.
Supported fields include Query, Mutation, Subscription keys. See [https://graphql.org/](graphql docs) for further info.
*/
const resolvers = {
Query: {
images: async () => {
try {
const images = await Image.findAll();
return images;
} catch (error) {
throw new Error(error);
}
},
search: async (_, query) => {
try {
const { terms } = query;
const images = await Image.findAll({
where: {
name: {
[Op.iLike]: `%${terms}%`,
},
},
});
return images;
} catch (error) {
throw new Error(error);
}
},
},
Mutation: {
addImage: async (_, file) => {
try {
const image = new Image(file);
await image.save();
return image;
} catch (error) {
throw new Error(error);
}
},
},
};
module.exports = resolvers;

Here we are importing the Sequelize model for Image that we previously defined. The important thing here is that we have keys for Query and Mutation, which are mandatory fields required by the makeExecutableSchema function we’ll use later. As you can see in the resolvers file, this is where our database operations happen. Here is where I really like using an ORM like Sequelize since it makes database operations very simple and easy to read. There is a bit of magic happening in the search query, where we are preforming a “fuzzy” type search using the iLike operator supplied by postgres. While this certainly isn’t up to par with a true index based search like ElasticSearch or Solr, for our purposes it works just fine.

Now we can complete our index.js server file and combine all the pieces we created earlier. Below is the complete express server index.js file

const express = require("express");
const resolvers = require("./graphql/resolvers");
const { typeDefs } = require("./graphql/typeDefs");
const { graphqlHTTP } = require("express-graphql");
const { makeExecutableSchema } = require("graphql-tools");
const { PORT, BASE_URL } = require("./config");
const cors = require("cors");

const app = express();

app.use(cors());

app.use(express.json({ limit: "50mb" })); // limit required for TEXT images

const schema = makeExecutableSchema({
typeDefs,
resolvers,
});

app.all("/", (_, res) => res.redirect("/graphql"));

app.use(
"/graphql",
graphqlHTTP({
schema,
graphiql: true, // creates playground
})
);

app.listen(PORT, () => {
console.log(`🚀 Server ready at ${BASE_URL}:${PORT}`);
});

As you can see, this will start up our server and create our graphql schema and api. Opening a web browser to reach this server will redirect us to /graphql, showing us the graphiql playground. Note, saving images as base64 strings went beyond the query limit size allowed by express (250kb), so a limit was expressly set to override the default value.

graphiql playground

Creating the Client

Now that the server is set up we can move onto creating a react frontend. We will use create-react-app for easy scaffolding, but there are plenty of other options available if you choose to look elsewhere. Some of my other favorite boilerplates are React-Boilerplate, Next.js, Gatsby.js, and React Most Wanted.

npx create-react-app client
cd client
npm install
npm start

Now you should see the your CRA server is running on localhost:3000.

From here we can start by allowing users to upload files from their local filesystem. For that we can use a simple input element with a type set to file. Check out this MDN article for complete documentation of the input element as well as the FileReader documentation.

<input 
id="uploadButton" // used to access the FileList on upload
data-testid="uploadButton" // used for testing
type="file" // all that you really need
accept=".png,.jpg,.svg,.webp,.gif, .jpeg, .tiff, .gif" // optional
/>

In order to access the file(s) we can use the following snippet below.

const file = document.getElementById("uploadButton").files[0];

Note the FileList API can uploaded several files at once, but here we are only concerned with the zeroth index or first file.

Once we’re able to access our file we need to convert it to a base64 string. The snippet below will do just that using the FileReader API.

const reader = new FileReader(); 

reader.onloadend = async function onLoadCallback() {
const { result: base64String } = reader;
await uploadImage(
{ variables: { file: base64String, name, type } }
);
refetch();
};

reader.readAsDataURL(file);

Note that the onloadend method is called after we’ve invoked the reader.readAsDataURL method. Once we are inside the onloadend callback we call uploadImage, which is using apollo/client’s useMutation function.

import { useMutation } from "@apollo/client"; // inside component                                                                    const [uploadImage, { loading }] = useMutation(ADD_IMAGE);

This method looks similar to React.useState in that it’s using array destructuring, and it allows us to call the uploadImage function on command. The ADD_IMAGE query is written below.

export const ADD_IMAGE = gql`
mutation AddImage($file: String!, $name: String!, $type: String) {
addImage(file: $file, name: $name, type: $type) {
file
name
type
}
}
`;

Now when a user goes to upload an image, the file is converted to base64, and saved in our back end. Saving images complete! Now I’ll show you how I perform a basic search based of image name.

Here I’ve created a basic input box with two event handlers, one for updating the search value, and another for triggering the actual API call.

<input                               
type="input"
id="searchInputBox"
value={searchTerms}
data-testid="searchInput"
onChange={handleSearchTerms}
onKeyPress={handleQuery}
/>

For reference here are the two event handlers associated with the input box above. For simplicity the search will occur when a user hits enter, but this probably wouldn’t provide the best UX in a real production application.

const handleSearchTerms = (e) => {
const { target: { value } = { target: { value: "" } } } = e;
setSearchTerms(value);
};
const handleQuery = (e) => {
if (e.key === "Enter") {
search({ variables: { terms: searchTerms } });
}
};

As you can see in handleQuery the “search” function is the actual API call to our GraphQL API. For this I’m using useLazyQuery from @apollo/client.

// SEARCH query exported from ../../graphql/queries
export const SEARCH = gql`
query Search($terms: String!) {
search(terms: $terms) {
file
name
type
}
}
`;
............................................
import { useLazyQuery } from “@apollo/client”;// inside component
const [search, { loading, data = false }] = useLazyQuery(SEARCH);

Now that we can upload images, and later search for them on the front end, we’re all done! Below is a screenshot of the final product.

UI Rendering with image uploading and search functionality

This is my first technical write up, so if you found this useful or would like similar articles please up vote and let me know! If you have a project you’d like help on or would like professional development services please reach out at web@xo9.io. If you made it this far thanks so much for reading and have a wonderful day!

--

--

Mike Burton
0 Followers

Software Developer, Musician, and Photographer