Mailroom

This post is written in the form of a tutorial.

The target audience is a personal friend who has basic Javascript experience and wanted to learn how to build a CRUD app with a REST API. I documented my process so he could follow along.

This simple web app intends to solve a client's problem with interoffice mail. In their current system there are multiple paper forms and an elaborate filing system required for each article of mail sent out from the office. This simple webapp is meant to be a prototype for a paperless system in that particular office.

Ideation

Employees will fill out a simple form with the address information for their letters and parcels and our app will generate a QR code which they will print and physically attach to their mail.

When the mailroom receives this letter/parcel they will then scan the QR code which will query the database for the relevant address information.

The mailroom can then weigh the package, update the record with measurements, decide on the correct carrier and shipping methodology, generate the shipping label using all the data, and finally ship the package while sending a message to the sender their mail has been shipped.

Setup

We'll start by creating a new directory using the command line with $ mkdir mailroom and run $ npm install express-generator -g to install the tool that will allow us to skeleton out our app quickly.

Run $ express --no-view --git --force to skeleton an app with no templating engine (because we plan on using React) as well as an empty .gitignore file. We add the force flag because the directory currently contains this README markdown file.

Now we can begin installing packages for our app. First run $ npm install to install the packages required by express.

Now would also be a good time to initialize a git repository. Run $ git init and $ git add -A to add the beginnings of our app to the git repository, and make the first commit with $ git commit -m "initialize repo". Now our app is under version control and if we make a mistake adding new features, we can easily roll back to a time when our app worked!

Okay, lets test our express installation. Run $ npm start and open up a browser to http://localhost:3000 where you should see the following image. This means the installation was successful:

screenshot of express default page

Database

Let's set up the database. First, stop the server with CTRL-C and run $ npm install sqlite bluebird --save.

We will be using Sqlite for our database in this project as it can be run with minimal setup. I've also chosen to use the sqlite package instead of sqlite3 as this lets us use promises instead of callbacks to access the database. This is a more modern syntax and, in my opinion, easier to read and understand what's going on. Also, we installed bluebird to handle promises. Though not really necessary for this scale of project, it was something I wanted to learn.

Create a folder for migrations, which will be useful going forward when we want to modify the database schema.

$ mkdir migrations && cd migrations

Lets create our first migration $ touch 001-init.sql and write some SQL.

-- Up
CREATE TABLE parcels
(
    id INTEGER PRIMARY KEY,
    user_id TEXT,
    file_id TEXT,
    shipment_type TEXT,
    shipment_destination TEXT,
    shipment_speed TEXT,
    attn_name TEXT,
    attn_phone TEXT,
    attn_organization TEXT,
    street_address TEXT,
    city TEXT,
    state_or_province TEXT,
    country TEXT,
    postal_code TEXT,
    us_value_of_goods TEXT,
    us_content_declaration TEXT,
    barcode TEXT,
    carrier_tracking_number TEXT,
    shipment_weight INT,
    shipment_length INT,
    shipment_width INT,
    shipment_height INT,
    shipping_method TEXT,
    shipment_status TEXT,
    creation_date TEXT,
    received_date TEXT
);

INSERT INTO parcels
    (user_id, street_address, attn_name,
    attn_organization, city, state_or_province, country, postal_code, barcode)
VALUES
    ("1A", "123 Example Street", "Spiderman", "The Avengers", "New York City", "New York", "US", "12345", "f34c6658-818b-11e8-adc0-fa7ae01bbebc");

-- Down
DROP TABLE parcels;

Now we'll add our database connection in app.js (we can refactor it into a connection file later)

const Promise = require('bluebird');
const sqlite = require('sqlite');
const dbPromise = Promise.resolve()
  .then(() => sqlite.open('./mailroom.sqlite', { Promise }))
  .then(db => db.migrate({ force: 'last' }));

This will create a database file in our root directory (if it doesn't already exist), add some test data, open a connection, and use the latest migration to ensure our database is using the newest schema during development.

Writing Tests

We're going to write some tests to ensure our REST endpoints work as expected. For running tests we will install Mocha, Chai, and ChaiHTTP. Let's create a directory to keep our tests $ mkdir test then install the test suite:

$ npm install mocha -g && npm install mocha chai chai-http --save-dev

Routing

Lets start by creating a test file with $ touch test/routes.spec.js where we will add the tests for our API routes. First include the correct libraries and tell Chai to use the HTTP module.

const chai = require('chai');
const should = chai.should();
const chaiHttp = require('chai-http');
const server = require('../app');

chai.use(chaiHTTP);

GET

Now we can describe our first test. Let's ensure that when we hit the /api/v1/parcels URL we receive an array of all parcels in the database.

describe('API Routes', function() {
  describe('GET /api/v1/parcels', function() {
    it('should return all parcels', function(done) {
      chai
        .request(server)
        .get('/api/v1/parcels')
        .end(function(err, res) {
          res.should.have.status(200);
          res.should.be.json;
          res.body.should.be.a('array');
          done();
        });
    });
  });
});

Now if we run $ mocha on the command line to start our test we see the following error:

deans-Mac-Pro:mailroom dean$ mocha


  API Routes
    GET /api/v1/parcels
GET /api/v1/parcels 200 5.857 ms - 11
      1) should return all parcels


  0 passing (58ms)
  1 failing

  1) API Routes
       GET /api/v1/parcels
         should return all parcels:
     Uncaught AssertionError: expected 'text/html; charset=utf-8' to include 'application/json'
      at /Users/dean/Documents/projects/mailroom/test/routes.spec.js:16:25
      at Test.Request.callback (node_modules/superagent/lib/node/index.js:716:12)
      at IncomingMessage.parser (node_modules/superagent/lib/node/index.js:916:18)
      at endReadableNT (_stream_readable.js:1062:12)
      at process._tickCallback (internal/process/next_tick.js:152:19)

It's saying that we aren't receiving json back from the server, so let's correct that by first opening app.js and adding

// Routes
app.get('/api/v1/parcels', async (req, res, next) => {
  try {
    const db = await dbPromise;
    const parcels = await db.all('SELECT * FROM parcels');
    res.send(parcels);
  } catch (err) {
    next(err);
  }
});

So now when our client hits the /api/v1/parcels route the server opens a connection to the database, selects all parcels from our parcels table, and sends the result back to the client.

POST

Before we write the code that lets us add a new parcel, we need some helpers for generating the unique barcodes. Lets run $ npm install uuid -s and in app.js add

const uuidv4 = require('uuid/v4');

Lets write another test, inside the same 'API Routes' function block just underneath the GET /api/v1/parcels test, this time for posting a new parcel to the server:

describe('POST /api/v1/parcels', function() {
    it('should create one parcel', function(done) {
      chai
        .request(server)
        .post('/api/v1/parcels')
        .send({
          user_id: 'Dean',
          street_address: '1234 Main St',
          attn_name: 'test',
          attn_organization: 'test',
          city: 'test',
          state_or_province: 'test',
          country: 'test',
          postal_code: 'test',
          barcode: '0e2887fc-19c9-4bf5-a9fc-0c5f5a23a87c',
          shipment_status: 'test',
          creation_date: 'test',
        })
        .end(function(err, res) {
          res.should.have.status(200);
          done();
        });
    });

Notice how we don't have every field we added to our database? When we are creating a new parcel, this is the data the user will enter. When the mailroom later updates the parcel with the shipping information and the package dimensions they will update this record with the missing information.

And again we go into app.js to add a route:

app.post('/api/v1/parcels', async (req, res, next) => {
  try {
    const payload = {
      $user_id: req.body.user_id,
      $street_address: req.body.street_address,
      $attn_name: req.body.attn_name,
      $recipient_first_name: req.body.recipient_first_name,
      $recipient_last_name: req.body.recipient_last_name,
      $attn_organization: req.body.attn_organization,
      $city: req.body.city,
      $state_or_province: req.body.state_or_province,
      $country: req.body.country,
      $postal_code: req.body.postal_code,
      $uuid: uuid,
      $parcel_status: req.body.parcel_status,
      $creation_date: new Date().toISOString(),
    };
    const uuid = uuidv4();
    const db = await dbPromise;
    const parcels = await db.run(
      `INSERT INTO parcels (
            user_id,
            street_address,
            attn_name,
            attn_organization,
            city,
            state_or_province,
            country,
            postal_code,
            barcode,
            shipment_status,
            creation_date) 
        VALUES (       
            $user_id,
            $street_address,
            $attn_name,
            $attn_organization,
            $city,
            $state_or_province,
            $country,
            $postal_code,
            $barcode,
            $shipment_status,
            $creation_date)`,
      payload
    );
    res.send({
      message: 'Success!',
      barcode: uuid,
    });
  } catch (err) {
    next(err);
  }
});

Here when a client hits the /api/v1/parcels route with a POST request and supplies address information, it's saved in the database as a new record.

Notice how we are using uuidv4() to generate a unique code for each record. We return the uuid as the response, the idea here is we will use this string to generate the barcode in the front-end. We also use new Date().toISOString() to add a timestamp of when that record was created.

Instead of sending an array of values as the payload we can send an object with key:value pairs.

NOTE: Make sure to include the $ in front of the key otherwise sqlite won't recognize it as a placeholder! (multiple hours spent trying to figure out why this wasn't working...)

If you're wondering (like I did) why it seems like the POST request is updating and not inserting new values each time the test is run, it's because we added { force: 'last' } to re-run the latest migration every time we restart our server. So even though it looks like the barcode and creation_date are changing in place, it's really:

  1. Dropping the parcels table
  2. Inserting the two sample rows in our 001-init.sql file
  3. POSTing the new row with the new data

Keep that in mind!

PUT

When a package is received by the mailroom in real life they will scan the barcode and pull up the database record to add shipping information and package dimensions. But before we can make updates to parcels we need to be able to GET individual parcels by their barcode.

In routes.spec.js we can add a new test for the new route we are about to create:

describe('GET /api/v1/parcels/:barcode', function() {
  it('should return one parcel', function(done) {
    chai
      .request(server)
      .get('/api/v1/parcels/f34c6658-818b-11e8-adc0-fa7ae01bbebc')
      .end(function(err, res) {
        res.should.have.status(200);
        res.should.be.json;
        res.body.should.be.a('object');
        done();
      });
  });
});

Here we are using a barcode from one of our sample rows in the 001-init.sql file as these are static and easy to reference.

Run the test with $ mocha and see that it fails with a 404. Now we need to write the route. In app.js under our other routes add:

app.get('/api/v1/parcels/:barcode', async (req, res, next) => {
  try {
    const db = await dbPromise;
    const parcels = await db.get(
      'SELECT * FROM parcels WHERE barcode = ?',
      req.params.barcode
    );
    res.send(parcels);
  } catch (err) {
    next(err);
  }
});

Rerun the test and... voila! 3 passing tests.

You can even verify in the browser by entering http://localhost:3000/api/v1/parcels/f34c6658-818b-11e8-adc0-fa7ae01bbebc to see our test data.

Now that the mailroom can access a single record, we need to be able to update that record. Let's write a test. Because sqlite doesn't return the inserted row as an object, it only returns the number of rows modified, we'll test that the number of modified rows is non-zero:

describe('PUT /api/v1/parcels/:barcode', function() {
  it('should update one parcel', function(done) {
    chai
      .request(server)
      .put('/api/v1/parcels/f34c6658-818b-11e8-adc0-fa7ae01bbebc')
      .send({
        shipment_weight: '1',
        shipment_length: '2',
        shipment_width: '3',
        shipment_height: '4',
        shipping_method: 'Expedited',
        shipment_status: 'Received',
      })
      .end(function(err, res) {
        res.should.have.status(200);
        res.should.be.json;
        res.body.should.not.equal(0);
        done();
      });
  });
});

Run the test and see that it fails. That's a good sign. Now we write the route. This looks similar to the previous route we made:

app.put('/api/v1/parcels/:barcode', async (req, res, next) => {
  try {
    const payload = {
      $barcode: req.params.barcode,
      $shipment_weight: req.body.shipment_weight,
      $shipment_length: req.body.shipment_length,
      $shipment_width: req.body.shipment_width,
      $shipment_height: req.body.shipment_height,
      $shipping_method: req.body.shipping_method,
      $shipment_status: req.body.shipment_status,
      $received_date: new Date().toISOString(),
    };
    const db = await dbPromise;
    const parcels = await db.run(
      `UPDATE parcels SET
            shipment_weight=$shipment_weight,
            shipment_length=$shipment_length,
            shipment_width=$shipment_width,
            shipment_height=$shipment_height,
            shipping_method=$shipping_method,
            shipment_status=$shipment_status,
            received_date=$received_date
            WHERE barcode = $barcode`,
      payload
    );
    res.json(parcels.changes);
  } catch (err) {
    next(err);
  }
});

DELETE

Okay, now we have the ability to CREATE, READ, and UPDATE. Our envisioned use case doesn't require users deleting records, in fact I think we would like to maintain old records for archival purposes. However, for completeness (and GDPR compliance!) we'll add a simple DELETE route.

Let's first add another sample into our 001-init.sql file with a static barcode. Add this ABOVE the previous insert, as we will see why in a second.

INSERT INTO parcels
    (user_id, street_address, attn_name,
    attn_organization, city, state_or_province, country, postal_code, barcode)
VALUES
    ("1A", "I SHOULD BE DELETED", "Iron Man", "The Avengers", "New York City", "New York", "US", "12345", "f34c6658-818b-11e8-adc0-fa7ae01bbebb");

Now let's write a test in our routes.spec.js file for our deletion:

describe('DELETE /api/v1/parcels/:barcode', function() {
  it('should delete one parcel', function(done) {
    chai
      .request(server)
      .delete('/api/v1/parcels/f34c6658-818b-11e8-adc0-fa7ae01bbebb')
      .end(function(err, res) {
        res.should.have.status(200);
        res.should.be.json;
      });
    chai
      .request(server)
      .get('/api/v1/parcels/')
      .end(function(err, res) {
        res.should.have.status(200);
        res.should.be.json;
        res.body.should.be.a('array');
        res.body[0].should.have.property('barcode');
        res.body[0].barcode.should.equal(
          'f34c6658-818b-11e8-adc0-fa7ae01bbebc'
        );
      });
    done();
  });
});

Here we are sending a request to the server to delete the parcel with "I SHOULD BE DELETED" as an address (note the 'bb' at the end of my barcode instead of the 'bc').

The second request to the server is to get back the full list of parcels, and if the delete works as intended, the first parcel (in position [0]) should now be our parcel with barcode ending in 'bc'.

Run the test. It fails. Now write the route.

app.delete('/api/v1/parcels/:barcode', async (req, res, next) => {
  try {
    const db = await dbPromise;
    const parcels = await db.run(
      'DELETE FROM parcels WHERE barcode = ?',
      req.params.barcode
    );
    res.json(parcels);
  } catch (err) {
    next(err);
  }
});

Run the tests again...

deans-Mac-Pro:mailroom dean$ mocha


  API Routes
    GET /api/v1/parcels
GET /api/v1/parcels 200 5.799 ms - 1037
      ✓ should return all parcels
    GET /api/v1/parcels/:barcode
GET /api/v1/parcels/f34c6658-818b-11e8-adc0-fa7ae01bbebc 200 1.022 ms - 517
      ✓ should return one parcel
    DELETE /api/v1/parcels/:barcode
      ✓ should delete one parcel
    PUT /api/v1/parcels/:barcode
DELETE /api/v1/parcels/f34c6658-818b-11e8-adc0-fa7ae01bbebb 200 2.549 ms - 79
GET /api/v1/parcels/ 200 2.028 ms - 519
PUT /api/v1/parcels/f34c6658-818b-11e8-adc0-fa7ae01bbebc 200 1.289 ms - 1
      ✓ should update one parcel
    POST /api/v1/parcels
POST /api/v1/parcels 200 1.686 ms - 36
      ✓ should create one parcel


  5 passing (79ms)

And it works! Now we have a fully functional basic CRUD API.

Front End

I want the front end built with React to take advantage of component architecture, however to quickly test that everything works let's start with a simple HTML page.

We should already have a page called index.html in our ./public folder. Let's open that up and add a basic form for our users.

User view

Here's what index.html should look like:

<html>
  <head>
    <title>Express</title>
    <link rel="stylesheet" href="/stylesheets/style.css" />
  </head>

  <body>
    <h1>Express</h1>
    <p>Welcome to Express</p>

    <form action="/api/v1/parcels" method="post">
      User ID:
      <input type="text" name="user_id" />
      <input type="submit" value="Submit" />
    </form>
  </body>
</html>

Now open http://localhost:3000 and try it out!

screenshot of index

So what's happening here is when you type your User ID in the text field and hit the submit button we use the POST route we made earlier to create a new record in our database.

screenshot of barcode result

As we can see by the random string of numbers and letters we receive back, we are successfully viewing the barcode of our created entry.

At this point we will add additional fields from our database.

We will also add any remaining fields to the routes.

Now our webpage looks like this!

screenshot of user view

(I also just learned how to take nicer screenshots of the selected window)

And our fields are nicely populating in the database.

screenshot of JSON from database

Mailroom View

We want our mailroom to be able to input a barcode, view a record, add additional information, and save it to the database.

Lets create a new folder and HTML file $ touch mailroom/index.html and open it up to add the following:

<html>
  <head>
    <title>Express</title>
    <link rel="stylesheet" href="/stylesheets/style.css" />
  </head>

  <body>
    <h1>Mailroom View</h1>
    <p>Please enter a barcode to retrieve a record</p>
    <form action="/api/v1/parcels" method="get">
      Barcode:
      <input type="text" name="barcode" />
      <input type="submit" value="Submit" />
    </form>
  </body>
</html>

Now navigate to http://localhost:3000/mailroom/ and see that it appears!

screenshot of mailroom view

If we enter a barcode from our database and hit submit...

Uh oh. screenshot of mailroom view

We get back an array of all our parcels. Not what we're looking for. If we look in the address bar we can see why this is happening: the barcode is being sent to the /api/v1/parcels route as a query parameter instead of using the /api/v1/:barcode route we defined earlier.

Now we can decide to either

  • use the barcode as a query parameter and refactor our routes, or
  • we can see if the form can submit to our /api/v1/:barcode route as a path variable

I'm not sure if there is a right answer as this design consideration seems to be subjective from the research I've done, but as our HTML form is presenting us with the query option by default we will go with that paradigm.

We will change our route to the following which will show us a specific parcel when a barcode is supplied as a query parameter.

app.get('/api/v1/parcels', async (req, res, next) => {
  try {
    const query = { $barcode: req.query.barcode };
    const db = await dbPromise;
    const parcels = await db.all(
      'SELECT * FROM parcels WHERE ($barcode IS NULL OR barcode = $barcode)',
      query
    );
    res.send(parcels);
  } catch (err) {
    next(err);
  }
});

Adding React

Now that we've tested with basic HTML we are going to add React as our frontend framework to easily handle dynamic content changes. Because we're looking to have the content rendered server-side we will also use Next.js.

Let's make a client directory to handle our client-side code

$ mkdir client && cd client

Then initialize a new package.json with $ npm init -y and install next with

$ npm install -s react react-dom next

If we add the following to our package.json

{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

and create a pages directory with an index.js file

$ mkdir pages && touch pages/index.js

now add a simple export

export default () => {
  return <div>Hello World</div>;
};

run $ npm run dev and open http://localhost:3030 to see our message!

Note: In my case, the default localhost:3000 was taken.

Let's rewrite our two pages as components. Starting with a users.js in our pages folder

import React, { Component } from 'react';

export default class UserPage extends Component {
  render() {
    return (
      <div>
        <h1>User View</h1>
        <p>Please fill out the following form to generate your shipping code</p>

        <form action='/api/v1/parcels' method='post'>
          <fieldset>
            <legend>User Information</legend>
            User ID:
            <input type='text' name='user_id' />
            <br /> File Number:
            <input type='text' name='file_id' />
          </fieldset>
          <br />
          <fieldset>
            <legend>Package Information</legend>
            <input type='radio' name='shipment_type' value='mail' /> Mail
            <input type='radio' name='shipment_type' value='parcel' /> Parcel
            <br /> Destination:
            <select name='shipment_locale'>
              <option value='local'>Within the Lower Mainland</option>
              <option value='national'>Within Canada</option>
              <option value='international'>International</option>
            </select>
            <br /> Shipping Speed:
            <select name='shipment_speed'>
              <option value='one'>One</option>
              <option value='two'>Two</option>
              <option value='three'>Three</option>
            </select>
          </fieldset>
          <br />
          <fieldset>
            <legend>Recipient Information</legend>
            Recipient Name:
            <input type='text' name='attn_name' />
            <br /> Phone Number:
            <input type='tel' name='attn_phone' />
            <br /> Organization:
            <input type='text' name='attn_organization' />
          </fieldset>
          <br />
          <fieldset>
            <legend>Shipping Address</legend>
            Address:
            <input type='text' name='street_address' />
            <br /> City:
            <input type='text' name='city' />
            <br /> Province/State:
            <input type='text' name='state_or_province' />
            <br /> Country:
            <input type='text' name='country' />
            <br /> Postal Code:
            <input type='text' name='postal_code' />
          </fieldset>
          <br />
          <input type='submit' value='Submit' />
        </form>
      </div>
    );
  }
}

Which will now be available at http://localhost:3030/user

And make a new file for our mailroom at /pages/mailroom.js

const MailroomPage = () => {
  return (
    <div>
      <h1>Mailroom View</h1>
      <p>Please enter a barcode to retrieve a record</p>
      <form action='/api/v1/parcels' method='get'>
        Barcode:
        <input type='text' name='barcode' />
        <input type='submit' value='Submit' />
      </form>
    </div>
  );
};

export default MailroomPage;

Which will now be available at http://localhost:3030/mailroom

Showing the QR code on submission

Next steps are to handle the form using React's "controlled components" and display the resulting barcode as a QR code on the same page.

We'll start by adding a constructor and input handler to our userpage component

export default class UserPage extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user_id: '',
      file_id: '',
      shipment_type: '',
      shipment_locale: '',
      shipment_speed: '',
      attn_name: '',
      attn_phone: '',
      attn_organization: '',
      street_address: '',
      city: '',
      state_or_province: '',
      country: '',
      postal_code: ''
    };

    this.handleChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({ [name]: value });
  }
  ...

and on each <input> we will add value={this.state.###} onChange={this.handleChange} where ### is the name of that input, for example:

<input
  type='text'
  name='user_id'
  value={this.state.user_id}
  onChange={this.handleChange}
/>

our Radio buttons will look slightly different:

<input
  type='radio'
  name='shipment_type'
  value='mail'
  checked={this.state.shipment_type === 'mail'}
  onChange={this.handleChange}
/>

as will our Select options:

<select
  name='shipment_locale'
  value={this.state.shipment_locale}
  onChange={this.handleChange}
>
  <option value='local'>Within the Lower Mainland</option>
  <option value='national'>Within Canada</option>
  <option value='international'>International</option>
</select>

Now if we were to check in React Developer Tools (a Chrome addon) we should see all our state values populating correctly

screenshot of React Developer Tools

Now lets change the form behaviour. The line

<form action="/api/v1/parcels" method="post">

changes to

<form onSubmit={this.handleSubmit}>

and we'll have to write a function to handle the POST request in Javascript.

Add the following to our constructor: this.handleSubmit = this.handleSubmit.bind(this); and create the function:

  handleSubmit(event) {
    alert('Test' + JSON.stringify(this.state));
    event.preventDefault();
  }

And now when we hit the Submit button we should see our data appear in an alert box in the browser window.

CORS

Let's modify the handleSubmit function to post JSON to our API route.

  handleSubmit(event) {
    fetch(`http://localhost:3000/api/v1/parcels`, {
      method: 'POST',
      mode: 'cors',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(this.state)
    })
      .then(response => response.json())
      .then(data => {
        console.log(data.message);
        this.setState({ barcode: data.barcode });
      })
      .catch(error => console.error(error));
    event.preventDefault();

Now we run into a CORS (Cross Origin Resource Sharing) issue. This is because, in my case, the API server is running on localhost:3000 and the nextjs server is running on localhost:3030

To solve this: in our root project directory (ie. not in the client folder) run $ npm install -s cors. Open app.js and add

const cors = require('cors');

app.use(cors());

now restart the server $ npm start and try the request from the client again.

We should see a "Success!" message in our console, and our barcode in our component's state!

screenshot of React Developer Tools - success

The QR Code

Next up: creating a component that will display our QR code.

Let start with a components folder in our client directory $ mkdir client/components and create a file called label.js

In label.js we'll create a simple functional component that accepts props.

import React from 'react';

export default props => {
  return <div>{props.barcode}</div>;
};

In user.js we'll add to the top:

import Label from '../components/label';

and under the closing form tag add a <Label barcode={this.state.barcode} /> tag.

Now when we submit the form, our barcode string appears!

In our client directory, run $ npm install -s bwip-js which is the library that will allow us to generate QR codes.

In label.js add our import statement and modify the functional component to a class component:

import React, { Component } from 'react';
import bwipjs from 'bwip-js';

export default class Label extends Component {
  constructor(props) {
    super(props);
  }

  componentDidUpdate() {
    bwipjs(
      'target-canvas',
      {
        bcid: 'qrcode',
        text: this.props.barcode,
      },
      (err, cvs) => {
        if (err) {
          console.error(err);
        }
      }
    );
  }

  render() {
    return (
      <div>
        <canvas id='target-canvas' />
      </div>
    );
  }
}

Now fill out the form and click submit. Voila! A QR code appears.

The Mailroom View

To make the next part easier, I made a component that lists all existing records in the database.

import React, { Component } from 'react';

export default class LabelList extends Component {
  constructor(props) {
    super(props);

    this.state = { data: [] };
  }

  componentDidMount = () => {
    fetch(`http://localhost:3000/api/v1/parcels`)
      .then(response => response.json())
      .then(data => this.setState({ data: data }))
      .catch(err => console.error(err));
  };

  render() {
    return (
      <div>
        <h3>All Records</h3>
        {this.state.data.map(label => {
          return <div key={label.id}>{label.barcode}</div>;
        })}
      </div>
    );
  }
}

then import LabelList from '../components/labelList'; and add <LabelList /> under the form in the mailroom view.

screenshot of mailroom barcode list

Now it will be easy to grab a barcode to test.

Lets create a component to render our retrieved record from the database:

// Record.js
import React, { Component } from 'react';

export default class Record extends Component {
  constructor(props) {
    super(props);

    this.state = {};
  }

  render() {
    return <div>{this.props.data.barcode}</div>;
  }
}

And let's modify the mailroom with some of the same code we used in the user view, handleSubmit and handleChange

// mailroom.js
import React, { Component } from 'react';
import LabelList from '../components/labelList';
import Record from '../components/Record';

export class MailroomPage extends Component {
  constructor(props) {
    super(props);
    this.state = { barcode: '', data: {} };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(event) {
    fetch(`http://localhost:3000/api/v1/parcels?barcode=${this.state.barcode}`)
      .then(response => response.json())
      .then(data => {
        this.setState({ data: data[0] });
      })
      .catch(error => console.error(error));
    event.preventDefault();
  }

  handleChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({ [name]: value });
  }

  render() {
    return (
      <div>
        <h1>Mailroom View</h1>
        <p>Please enter a barcode to retrieve a record</p>
        <form onSubmit={this.handleSubmit}>
          Barcode:
          <input
            type='text'
            name='barcode'
            value={this.state.attn_name}
            onChange={this.handleChange}
          />
          <input type='submit' value='Submit' />
        </form>
        <Record data={this.state.data} />
        <LabelList />
      </div>
    );
  }
}

export default MailroomPage;

Now when we submit a barcode, we should see the name of our recipient.

Fleshing things out

Here are some easy steps I completed without documenting:

  • Header component with navigation links to make it easy to swap between views.
  • More information on record retrieval in the Record component.
  • More fields in the Label component to give the user feedback on their submission

Next Steps

And the next steps for the project:

  1. Add the ability for the Mailroom to edit retrieved information and update the database.
  2. Autofocus the Barcode entry field to make it easier to use a QR Code scanner
  3. Format the Label the user creates to make it nicely printable.
  4. Add authentication so only authorized users can access the Mailroom.