Create Word Documents with Node.js
Tags: webdev, node, javascript, tutorial
Published: 2020-01-20 00:46:30

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

**Follow me on Twitter at https://twitter.com/AuMayeung**

**Many more articles at https://medium.com/@hohanga**

Microsoft Word is a very popular word processor. It is a popular format for making and exchanging documents. Therefore, a feature of a lot of apps is to create Word documents from other kinds of documents. Creating Word documents is easy in Node.js apps with third-party libraries. We can make an app from it easily ourselves.

In this article, we will make an app that lets users enter their document in a rich text editor and generate a Word document from it. We will use Express for back end and React for front end.

Back End
========

We will start with the back end. To start, we will create a project folder with the `backend` folder inside. Then in the `backend` folder run `npx express-generator` to create the Express app. Then run `npm i` to install the packages Next we install our own packages. We need Babel for run the app with the latest version of JavaScript, CORS for cross domain requests with front end, HTML-DOCX-JS for converting HTML strings to Word documents, Multer for file upload, Sequelize for ORM, and SQLite3 for our database.

We install all of these by running `npm i @babel/cli @babel/core @babel/node @babel/preset-env cors html-docx-js sequelize sqlite3 multer` .

After that we change the `scripts` section of `package.json` to have:

```json
"start": "nodemon --exec npm run babel-node --  ./bin/www",  
"babel-node": "babel-node"
```

so that we run our app with Babel instead of the regular Node runtime.

Then create a `.babelrc` file in the `backend` folder and add:

```json
{
    "presets": [
        "@babel/preset-env"
    ]
}
```

to specify that we run our app with the latest version of JavaScript.

Next we add our database code. Run `npx sequelize-cli init` in the `backend` folder to create the Sequelize code.

We should have a `config.js` in the project now. In there, add:

```json
{  
  "development": {  
    "dialect": "sqlite",  
    "storage": "development.db"  
  },  
  "test": {  
    "dialect": "sqlite",  
    "storage": "test.db"  
  },  
  "production": {  
    "dialect": "sqlite",  
    "storage": "production.db"  
  }  
}
```

to specify that we SQLite for our database.

Next create our model and migration by running:

```bash
npx sequelize-cli model:create --name Document --attributes name:string,document:text,documentPath:string
```

to create a `Document` model and `Documents` table.

We then run:

```bash
npx sequelize-cli db:migrate
```

to create the database.

Next we create our routes. Create a `document.js` file in the `routes` folder and add:

```javascript
var express = require("express");
const models = require("../models");
var multer = require("multer");
const fs = require("fs");
var router = express.Router();
const htmlDocx = require("html-docx-js");
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./files");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});
const upload = multer({
  storage
});
router.get("/", async (req, res, next) => {
  const documents = await models.Document.findAll();
  res.json(documents);
});
router.post("/", async (req, res, next) => {
  const document = await models.Document.create(req.body);
  res.json(document);
});
router.put("/:id", async (req, res, next) => {
  const id = req.params.id;
  const { name, document } = req.body;
  const doc = await models.Document.update(
    { name, document },
    { where: { id } }
  );
  res.json(doc);
});
router.delete("/:id", async (req, res, next) => {
  const id = req.params.id;
  await models.Document.destroy({ where: { id } });
  res.json({});
});
router.get("/generate/:id", async (req, res, next) => {
  const id = req.params.id;
  const documents = await models.Document.findAll({ where: { id } });
  const document = documents[0];
  const converted = htmlDocx.asBlob(document.document);
  const fileName = `${+new Date()}.docx`;
  const documentPath = `${__dirname}/../files/${fileName}`;
  await new Promise((resolve, reject) => {
    fs.writeFile(documentPath, converted, err => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
  const doc = await models.Document.update(
    { documentPath: fileName },
    { where: { id } }
  );
  res.json(doc);
});
router.post("/uploadImage", upload.single("upload"), async (req, res, next) => {
  res.json({
    uploaded: true,
    url: `${process.env.BASE_URL}/${req.file.filename}`
  });
});
module.exports = router;
```

We do the standard CRUD operations to the `Documents` table in the first 4 routes. We have GET for getting all the `Documents` , POST for create a `Document` from post parameters, PUT for updating `Document` by ID, DELETE for deleting a `Document` by looking it up by ID. We have the HTML in the `document` field for generating the Word document later.

The `generate` route is for generating the Word document. We get the ID from the URL and then use the HTML-DOCX-JS package to generate a Word document. We generate the Word document by converting the HTML document to a file stream object with the HTML-DOCX-JS package and then writing the stream to a file and saving the path to the file in the `Document` entry in with the ID in the URL parameter.

We also have a `uploadImage` route to let user upload images with CKEditor with the CKFinder plugin. The plugin expects `uploaded` and `url` in the response so we return those.

Then we need to add a `files` folder in the `backend` folder.

Next in `app.js` , we replace the existing code with:

```javascript
require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var cors = require("cors");
var indexRouter = require("./routes/index");
var documentRouter = require("./routes/document");
var app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(express.static(path.join(__dirname, "files")));
app.use(cors());
app.use("/", indexRouter);
app.use("/document", documentRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
  res.status(err.status || 500);
  res.render("error");
});
module.exports = app;
```

We expose the file folder with:

```javascript
app.use(express.static(path.join(__dirname, "files")));
```

and expose the `document` route with:

```javascript
var documentRouter = require("./routes/document");  
app.use("/document", documentRouter);
```

Front End
=========

Now the back end is done, we can move onto the front end. Create the React app by running Create React App. We run `npx create-react-app frontend` in the root folder of the project.

We then install our packages. We will use CKEditor for our rich text editor, Axios for making HTTP requests, Bootstrap for styling, MobX for simple state management, React Router for routing URLs to components, and Formik and Yup for form value handling and form validation respectively.

Install all the packages by running `npm i @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-react axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup` .

With the packages installed, we can get started. In `App.js` , we replace the existing code with:

```jsx
import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import TopBar from "./TopBar";
import { DocumentStore } from "./store";
import "./App.css";
const history = createHistory();
const documentStore = new DocumentStore();
function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} documentStore={documentStore} />
          )}
        />
      </Router>
    </div>
  );
}
export default App;
```

to add our top bar and route to the home page.

In `App.css` , we replace the existing code with:

```css
.page {
  padding: 20px;
}
.content-invalid-feedback {
  width: 100%;
  margin-top: 0.25rem;
  font-size: 80%;
  color: #dc3545;
}
nav.navbar {
  background-color: green !important;
}
```

to add some padding to our page and style the validation message for the Rich text editor, and change the color of the `navbar`.

Next we create the form for adding and editing documents. Create a `DocumentForm.js` in the `src` file and add:

```jsx
import React from "react";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import { Formik, Field } from "formik";
import { addDocument, editDocument, getDocuments, APIURL } from "./request";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
const schema = yup.object({
  name: yup.string().required("Name is required")
});
function DocumentForm({ documentStore, edit, onSave, doc }) {
  const [content, setContent] = React.useState("");
  const [dirty, setDirty] = React.useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid || !content) {
      return;
    }
    const data = { ...evt, document: content };
    if (!edit) {
      await addDocument(data);
    } else {
      await editDocument(data);
    }
    getAllDocuments();
  };
  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    onSave();
  };
  return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? doc : {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="name">
                <Form.Label>Name</Form.Label>
                <Form.Control
                  type="text"
                  name="name"
                  placeholder="Name"
                  value={values.name || ""}
                  onChange={handleChange}
                  isInvalid={touched.name && errors.name}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.name}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="content">
                <Form.Label>Content</Form.Label>
                <CKEditor
                  editor={ClassicEditor}
                  data={content || ""}
                  onInit={editor => {
                    if (edit) {
                      setContent(doc.document);
                    }
                  }}
                  onChange={(event, editor) => {
                    const data = editor.getData();
                    setContent(data);
                    setDirty(true);
                  }}
                  config={{
                    ckfinder: {
                      uploadUrl:
                        `${APIURL}/document/uploadImage`
                    }
                  }}
                />
                <div className="content-invalid-feedback">
                  {dirty && !content ? "Content is required" : null}
                </div>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: 10 }}>
              Save
            </Button>
            <Button type="button">Cancel</Button>
          </Form>
        )}
      </Formik>
    </>
  );
}
export default observer(DocumentForm);
```

We wrap our React Bootstrap `Form` inside the `Formik` component to get the form handling function from Formik which we use directly in the React Bootstrap form fields. We cannot do the same with CKEditor, so we write our own form handlers for the rich text editor. We set the `data` prop in the `CKEditor` to set the value of the input of the rich text editor. The `onInit` function is used with users try to edit an existing document since we have to set the `data` prop with the editor initializes by running `setContent(doc.document);` . The `onChange` prop is the handler function for setting `content` whenever it is updated so the `data` prop will have the latest value, which we will submit when the user clicks Save.

We use the CKFinder plugin to upload images. To make it work, we set the image upload URL to the URL of the upload route in our back end.

The form validation schema is provided by the Yup `schema` object which we create at the top of the code. We check if the `name` field is filled in.

The `handleSubmit` function is for handling submitting the data to back end. We check both the `content` and the `evt` object to check both fields since we cannot incorporate the Formik form handlers directly into the `CKEditor` component.

If everything is valid, then we add a new document or update it depending on on whether the `edit` prop is true or not.

Then when saving is successful, we call `getAllDocuments` to populate the latest documents into our MobX store by running `documentStore.setDocuments(response.data);` .

Next we make our home page by creating `HomePage.js` in the `src` folder and add:

```jsx
import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import DocumentForm from "./DocumentForm";
import Modal from "react-bootstrap/Modal";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getDocuments, deleteDocument, generateDocument, APIURL } from "./request";
function HomePage({ documentStore, history }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [doc, setDoc] = useState([]);
  const openAddTemplateModal = () => {
    setOpenAddModal(true);
  };
  const closeAddModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
  };
  const cancelAddModal = () => {
    setOpenAddModal(false);
  };
  const cancelEditModal = () => {
    setOpenEditModal(false);
  };
  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    setInitialized(true);
  };
  const editDocument = d => {
    setDoc(d);
    setOpenEditModal(true);
  };
  const onSave = () => {
    cancelAddModal();
    cancelEditModal();
  };
  const deleteSingleDocument = async id => {
    await deleteDocument(id);
    getAllDocuments();
  };
  const generateSingleDocument = async id => {
    await generateDocument(id);
    alert("Document Generated");
    getAllDocuments();
  };
  useEffect(() => {
    if (!initialized) {
      getAllDocuments();
    }
  });
  return (
    <div className="page">
      <h1 className="text-center">Documents</h1>
      <ButtonToolbar onClick={openAddTemplateModal}>
        <Button variant="primary">Add Document</Button>
      </ButtonToolbar>
      <Modal show={openAddModal} onHide={closeAddModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            onSave={onSave.bind(this)}
            cancelModal={cancelAddModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <Modal show={openEditModal} onHide={cancelEditModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            edit={true}
            doc={doc}
            onSave={onSave.bind(this)}
            cancelModal={cancelEditModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>Name</th>
            <th>Document</th>
            <th>Generate Document</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {documentStore.documents.map(d => {
            return (
              <tr key={d.id}>
                <td>{d.name}</td>
                <td>
                  <a href={`${APIURL}/${d.documentPath}`} target="_blank">
                    Open
                  </a>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={generateSingleDocument.bind(this, d.id)}
                  >
                    Generate Document
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editDocument.bind(this, d)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSingleDocument.bind(this, d.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
}
export default withRouter(observer(HomePage));
```

We have a React Bootstrap table for listing the documents with buttons to edit, delete documents and generate Word document. Also, there is a Open link for opening the Word document in each row. We have a create button on top of the table.

When the page loads, we call `getAllDocuments` and populate them in the MobX store. We open and close the add and edit modals with the `openAddTemplateModal`, `closeAddModal`, `cancelAddModal`, `cancelEditModal` functions.

Next create `request.js` in the `src` folder and add:

```javascript
export const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getDocuments = () => axios.get(`${APIURL}/document`);
export const addDocument = data => axios.post(`${APIURL}/document`, data);
export const editDocument = data => axios.put(`${APIURL}/document/${data.id}`, data);
export const deleteDocument = id => axios.delete(`${APIURL}/document/${id}`);
export const generateDocument = id => axios.get(`${APIURL}/document/generate/${id}`);
```

to add the functions to make requests to our routes in the back end.

Then we create our MobX store. Create `store.js` in the `src` folder and put:

```javascript
import { observable, action, decorate } from "mobx";
class DocumentStore {
  documents = [];
setDocuments(documents) {
    this.documents = documents;
  }
}
DocumentStore = decorate(DocumentStore, {
  documents: observable,
  setDocuments: action
});
export { DocumentStore };
```

We have the function `setDocuments` to put the photo data in the store, which we used in `HomePage` and `DocumentForm` and we instantiated it before exporting so that we only have to do it in one place.

This block:

```javascript
DocumentStore = decorate(DocumentStore, {  
  documents: observable,  
  setDocuments: action  
});
```

designates the `documents` array in `DocumentStore` as the entity that can be watched by components for changes. The `setDocuments` function is designated as the function that can be used to set the `documents` array in the store.

Next we create the top bar by creating a `TopBar.js` file in the `src` folder and add:

```jsx
import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">Word App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={location.pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}
export default withRouter(TopBar);
```

This contains the React Bootstrap `Navbar` to show a top bar with a link to the home page and the name of the app. We only display it with the `token` present in local storage. We check the `pathname` to highlight the right links by setting the `active` prop.

Next in `index.html` , we replace the existing code with:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Word App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
```

to add the Bootstrap CSS and change the title.

After writing all that code, we can run our app. Before running anything, install `nodemon` by running `npm i -g nodemon` so that we don’t have to restart back end ourselves when files change.

Then run back end by running `npm start` in the `backend` folder and `npm start` in the `frontend` folder, then choose ‘yes’ if you’re asked to run it from a different port.