Categories
Koa Nodejs

How to Use Koa to Build Simple Web Apps

Koa is a simple web framework for building backend applications. By default, it comes with nothing. You get what you need by adding packages that extend the default functionality. This makes building apps easy and keeps the code as simple as possible.

In this article, we will build an address book app. Users can add, edit, and remove contacts and also view them in a table. We build the back end with Koa and a simple frontend with React.

To start the project, we create folder for it and inside it, we create a backend directory.

Back End

Now we can build the back end, we run npm init and answer the questions by entering the default values. Then we install our own packages. Koa comes with nothing, so we need to install a request body parser, a router, and a CORS add-on to enable cross domain requests. We also need to add libraries for the database.

To install all these packages, run npm i @babel/cli @babel/core @babel/node @babel/preset-env @koa/cors koa-bodyparser koa-router sequelize sqlite3. We need the Babel packages for running with the latest JavaScript features. Sequelize and SQLite are the ORM and database that we will use respectively. The Koa packages are for enabling CORS, parsing JSON request body, and enable routing respectively.

Next run:

npx sequelize-cli init

This creates the database boilerplate code.

Then we run:

npx sequelize-cli --name Contact --attributes firstName:string,lastName:string,address:string.city:string,region:string,country:string,postalCode:string,phone:string,email:string,age:number

This creates a Contacts table with the fields and data types listed in the attributes option.

After that is done, run npx sequelize-cli db:migrate to create the database.

Next create app.js in the root of the backend folder and add:

const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("koa-router");
const models = require("./models");
const bodyParser = require("koa-bodyparser");

const app = new Koa();
app.use(bodyParser());
app.use(cors());
const router = new Router();

router.get("/contacts", async (ctx, next) => {
  const contacts = await models.Contact.findAll();
  ctx.body = contacts;
});

router.post("/contacts", async (ctx, next) => {
  const contact = await models.Contact.create(ctx.request.body);
  ctx.body = contact;
});

router.put("/contacts/:id", async (ctx, next) => {
  const { id, ...body } = ctx.request.body;
  const contact = await models.Contact.update(body, { where: { id } });
  ctx.body = contact;
});

router.delete("/contacts/:id", async (ctx, next) => {
  const id = ctx.params.id;
  await models.Contact.destroy({ where: { id } });
  ctx.body = {};
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

This is the file with all the logic for our app. We use the Sequelize model we created by importing the models module that is created by running sequelize-cli init.

Then we enable CORS by adding app.use(cors()); JSON request body parsing is enabled by adding app.use(bodyParser());. We add a router by adding: const router = new Router(); .

In the GET contacts route, we get all the contacts. The POST is for adding a contact. The PUT route is used for updating an existing contact by looking it up by ID. And the DELETE route is for deleting a contact by ID.

Now the back end is done. It is that simple.

Front End

To start we need to run Create React App to scaffold the app. We run npx create-react-app address-book to create the app project folder with the initial files. The app will have a home page to display the contacts and let us open a modal to add a contact. There will be a table that displays all the contacts and Edit and Delete buttons on each row to edit or delete each contact. The contacts will store in a central Redux store to store the contacts in a central place, making them easy to access. React Router will be used for routing. Contacts will be saved in the back end spawned using the JSON server package, located at https://github.com/typicode/json-server.

For form validation, then you need to use a third party library. Formik and Yup work great together to allow us to take care of most form validation needs. Formik lets us build the forms and display the errors, and handle form value changes, which is another thing we have to do all my hand otherwise. Yup let us write a schema for validating our form fields. It can check almost anything, with common validation code like email and required fields available as built in functions. It can also check for fields that depend on other fields, like the postal code format depending on country. Bootstrap forms can be used seamlessly with Formik and Yup.

Once that is done, we have to install some libraries. To install the libraries we mentioned above, we run npm i axios bootstrap formik react-bootstrap mobx mobx-react react-router-dom yup . Axios is the HTTP client that we use for making HTTP requests to back end. react-router-dom is the package name for the latest version of React Router.

Now that we have all the libraries installed, we can start building the app. All files will be in the src folder except mentioned otherwise. First we work on the MobX store. We create a file called store.js in the src folder and add the following:

import { observable, action, decorate } from "mobx";

class ContactsStore {
  contacts = [];

  setContacts(contacts) {
    this.contacts = contacts;
  }
}

ContactsStore = decorate(ContactsStore, {
  contacts: observable,
  setContacts: action,
});

export { ContactsStore };

This is a simple store which stores the contacts the contacts array is where we store the contacts for the whole app. The setContacts function let us set contacts from any component where we pass in the this store object to.

This block:

ContactsStore = decorate(ContactsStore, {
  contacts: observable,
  setContacts: action,
});

designates the contacts array in ContactsStore as the entity that can be watched by components for changes. The setContacts is designated as the function that can be used to set the contacts array in the store.

In App.js , we replace what is existing with the following:

import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
const history = createHistory();

function App({ contactsStore }) {
  return (
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Address Book App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} contactsStore={contactsStore} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

We pass the store into any component that needs it like the HomePage , which will pass the component to the ContactForm .

This is where we add the navigation bar and show our routes routed by the React Router. In App.css , we replace the existing code with:

.App {
  text-align: center;
}

This centers the text.

Next we build our contact form. This is the most logic heavy part of our app. We create a file called ContactForm.js and add:

import React from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import InputGroup from "react-bootstrap/InputGroup";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import { COUNTRIES } from "./exports";
import PropTypes from "prop-types";
import { addContact, editContact, getContacts } from "./requests";

const schema = yup.object({
  firstName: yup.string().required("First name is required"),
  lastName: yup.string().required("Last name is required"),
  address: yup.string().required("Address is required"),
  city: yup.string().required("City is required"),
  region: yup.string().required("Region is required"),
  country: yup
    .string()
    .required("Country is required")
    .default("Afghanistan"),
  postalCode: yup
    .string()
    .when("country", {
      is: "United States",
      then: yup
        .string()
        .matches(/^[0-9]{5}(?:-[0-9]{4})?$/, "Invalid postal code"),
    })
    .when("country", {
      is: "Canada",
      then: yup
        .string()
        .matches(
          /^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/,
          "Invalid postal code"
        ),
    })
    .required(),
  phone: yup
    .string()
    .when("country", {
      is: country => ["United States", "Canada"].includes(country),
      then: yup
        .string()
        .matches(/^[2-9]d{2}[2-9]d{2}d{4}$/, "Invalid phone nunber"),
    })
    .required(),
  email: yup
    .string()
    .email("Invalid email")
    .required("Email is required"),
  age: yup
    .number()
    .required("Age is required")
    .min(0, "Minimum age is 0")
    .max(200, "Maximum age is 200"),
});

function ContactForm({
  edit,
  onSave,
  contact,
  onCancelAdd,
  onCancelEdit,
  contactsStore,
}) {
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    if (!edit) {
      await addContact(evt);
    } else {
      await editContact(evt);
    }
    const response = await getContacts();
    contactsStore.setContacts(response.data);
    onSave();
  };

  return (
    <div className="form">
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={contact || {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="firstName">
                <Form.Label>First name</Form.Label>
                <Form.Control
                  type="text"
                  name="firstName"
                  placeholder="First Name"
                  value={values.firstName || ""}
                  onChange={handleChange}
                  isInvalid={touched.firstName && errors.firstName}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.firstName}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="lastName">
                <Form.Label>Last name</Form.Label>
                <Form.Control
                  type="text"
                  name="lastName"
                  placeholder="Last Name"
                  value={values.lastName || ""}
                  onChange={handleChange}
                  isInvalid={touched.firstName && errors.lastName}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.lastName}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="address">
                <Form.Label>Address</Form.Label>
                <InputGroup>
                  <Form.Control
                    type="text"
                    placeholder="Address"
                    aria-describedby="inputGroupPrepend"
                    name="address"
                    value={values.address || ""}
                    onChange={handleChange}
                    isInvalid={touched.address && errors.address}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.address}
                  </Form.Control.Feedback>
                </InputGroup>
              </Form.Group>
            </Form.Row>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="city">
                <Form.Label>City</Form.Label>
                <Form.Control
                  type="text"
                  placeholder="City"
                  name="city"
                  value={values.city || ""}
                  onChange={handleChange}
                  isInvalid={touched.city && errors.city}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.city}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="region">
                <Form.Label>Region</Form.Label>
                <Form.Control
                  type="text"
                  placeholder="Region"
                  name="region"
                  value={values.region || ""}
                  onChange={handleChange}
                  isInvalid={touched.region && errors.region}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.region}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="country">
                <Form.Label>Country</Form.Label>
                <Form.Control
                  as="select"
                  placeholder="Country"
                  name="country"
                  onChange={handleChange}
                  value={values.country || ""}
                  isInvalid={touched.region && errors.country}
                >
                  {COUNTRIES.map(c => (
                    <option key={c} value={c}>
                      {c}
                    </option>
                  ))}
                </Form.Control>
                <Form.Control.Feedback type="invalid">
                  {errors.country}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="postalCode">
                <Form.Label>Postal Code</Form.Label>
                <Form.Control
                  type="text"
                  placeholder="Postal Code"
                  name="postalCode"
                  value={values.postalCode || ""}
                  onChange={handleChange}
                  isInvalid={touched.postalCode && errors.postalCode}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.postalCode}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="phone">
                <Form.Label>Phone</Form.Label>
                <Form.Control
                  type="text"
                  placeholder="Phone"
                  name="phone"
                  value={values.phone || ""}
                  onChange={handleChange}
                  isInvalid={touched.phone && errors.phone}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.phone}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="email">
                <Form.Label>Email</Form.Label>
                <Form.Control
                  type="text"
                  placeholder="Email"
                  name="email"
                  value={values.email || ""}
                  onChange={handleChange}
                  isInvalid={touched.email && errors.email}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.email}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="age">
                <Form.Label>Age</Form.Label>
                <Form.Control
                  type="text"
                  placeholder="Age"
                  name="age"
                  value={values.age || ""}
                  onChange={handleChange}
                  isInvalid={touched.age && errors.age}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.age}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Save
            </Button>
            <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>
              Cancel
            </Button>
          </Form>
        )}
      </Formik>
    </div>
  );
}

ContactForm.propTypes = {
  edit: PropTypes.bool,
  onSave: PropTypes.func,
  onCancelAdd: PropTypes.func,
  onCancelEdit: PropTypes.func,
  contact: PropTypes.object,
  contactsStore: PropTypes.object
};

export default ContactForm;

We pass in the contactsStore from the HomePage component, allowing us to use the data and functions in contactsStore.

For form validation, we use Formik to facilitate building our contact form here, with our Boostrap Form component nested in the Formik component so that we can use Formik’s handleChange, handleSubmit, values, touched, and errors parameters. handleChange is a function that let us update the form field data from the inputs without writing the code ourselves. handleSubmit is the function that we passed into the onSubmit handler of the Formik component. The parameter in the function is the data we entered, with the field name as the key, as defined by the name attribute of each field and the value of each field as the value of those keys. Notice that in each value prop, we have ||'' so we do not get undefined values and prevent uncontrolled form warnings from getting triggered.

To display form validation messages, we have to pass in the isInvalid prop to each Form.Control component. The schema object is what Formik will check against for form validation. The argument in the required function is the validation error message. The second argument of the matches , min and max functions are also validation messages.

The parameter of the ContactForm function are props, which we will pass in from the HomePage component that we will build later. The handleSubmit function checks if the data is valid, then if it is then it will proceed to saving according to whether it is adding or editing a contact. Then when saving is successful we set the contacts in the store and call onSave prop, which is a function to close the modal the form is in. The modal will be defined in the home page.

Next we create a file called exports.js , and put:

export const COUNTRIES = [
  "Afghanistan",
  "Albania",
  "Algeria",
  "Andorra",
  "Angola",
  "Anguilla",
  "Antigua &amp; Barbuda",
  "Argentina",
  "Armenia",
  "Aruba",
  "Australia",
  "Austria",
  "Azerbaijan",
  "Bahamas",
  "Bahrain",
  "Bangladesh",
  "Barbados",
  "Belarus",
  "Belgium",
  "Belize",
  "Benin",
  "Bermuda",
  "Bhutan",
  "Bolivia",
  "Bosnia &amp; Herzegovina",
  "Botswana",
  "Brazil",
  "British Virgin Islands",
  "Brunei",
  "Bulgaria",
  "Burkina Faso",
  "Burundi",
  "Cambodia",
  "Cameroon",
  "Canada",
  "Cape Verde",
  "Cayman Islands",
  "Chad",
  "Chile",
  "China",
  "Colombia",
  "Congo",
  "Cook Islands",
  "Costa Rica",
  "Cote D Ivoire",
  "Croatia",
  "Cruise Ship",
  "Cuba",
  "Cyprus",
  "Czech Republic",
  "Denmark",
  "Djibouti",
  "Dominica",
  "Dominican Republic",
  "Ecuador",
  "Egypt",
  "El Salvador",
  "Equatorial Guinea",
  "Estonia",
  "Ethiopia",
  "Falkland Islands",
  "Faroe Islands",
  "Fiji",
  "Finland",
  "France",
  "French Polynesia",
  "French West Indies",
  "Gabon",
  "Gambia",
  "Georgia",
  "Germany",
  "Ghana",
  "Gibraltar",
  "Greece",
  "Greenland",
  "Grenada",
  "Guam",
  "Guatemala",
  "Guernsey",
  "Guinea",
  "Guinea Bissau",
  "Guyana",
  "Haiti",
  "Honduras",
  "Hong Kong",
  "Hungary",
  "Iceland",
  "India",
  "Indonesia",
  "Iran",
  "Iraq",
  "Ireland",
  "Isle of Man",
  "Israel",
  "Italy",
  "Jamaica",
  "Japan",
  "Jersey",
  "Jordan",
  "Kazakhstan",
  "Kenya",
  "Kuwait",
  "Kyrgyz Republic",
  "Laos",
  "Latvia",
  "Lebanon",
  "Lesotho",
  "Liberia",
  "Libya",
  "Liechtenstein",
  "Lithuania",
  "Luxembourg",
  "Macau",
  "Macedonia",
  "Madagascar",
  "Malawi",
  "Malaysia",
  "Maldives",
  "Mali",
  "Malta",
  "Mauritania",
  "Mauritius",
  "Mexico",
  "Moldova",
  "Monaco",
  "Mongolia",
  "Montenegro",
  "Montserrat",
  "Morocco",
  "Mozambique",
  "Namibia",
  "Nepal",
  "Netherlands",
  "Netherlands Antilles",
  "New Caledonia",
  "New Zealand",
  "Nicaragua",
  "Niger",
  "Nigeria",
  "Norway",
  "Oman",
  "Pakistan",
  "Palestine",
  "Panama",
  "Papua New Guinea",
  "Paraguay",
  "Peru",
  "Philippines",
  "Poland",
  "Portugal",
  "Puerto Rico",
  "Qatar",
  "Reunion",
  "Romania",
  "Russia",
  "Rwanda",
  "Saint Pierre &amp; Miquelon",
  "Samoa",
  "San Marino",
  "Satellite",
  "Saudi Arabia",
  "Senegal",
  "Serbia",
  "Seychelles",
  "Sierra Leone",
  "Singapore",
  "Slovakia",
  "Slovenia",
  "South Africa",
  "South Korea",
  "Spain",
  "Sri Lanka",
  "St Kitts &amp; Nevis",
  "St Lucia",
  "St Vincent",
  "St. Lucia",
  "Sudan",
  "Suriname",
  "Swaziland",
  "Sweden",
  "Switzerland",
  "Syria",
  "Taiwan",
  "Tajikistan",
  "Tanzania",
  "Thailand",
  "Timor L'Este",
  "Togo",
  "Tonga",
  "Trinidad &amp; Tobago",
  "Tunisia",
  "Turkey",
  "Turkmenistan",
  "Turks &amp; Caicos",
  "Uganda",
  "Ukraine",
  "United Arab Emirates",
  "United Kingdom",
  "United States",
  "United States Minor Outlying Islands",
  "Uruguay",
  "Uzbekistan",
  "Venezuela",
  "Vietnam",
  "Virgin Islands (US)",
  "Yemen",
  "Zambia",
  "Zimbabwe",
];

These are countries for the countries field in the form.

In HomePage.js , we put:

import React from "react";
import { useState, useEffect } from "react";
import Table from "react-bootstrap/Table";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import ContactForm from "./ContactForm";
import "./HomePage.css";
import { getContacts, deleteContact } from "./requests";
import { observer } from "mobx-react";

function HomePage({ contactsStore }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedContact, setSelectedContact] = useState({});

const openModal = () => {
    setOpenAddModal(true);
  };

const closeModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
    getData();
  };

const cancelAddModal = () => {
    setOpenAddModal(false);
  };

const editContact = contact => {
    setSelectedContact(contact);
    setOpenEditModal(true);
  };

const cancelEditModal = () => {
    setOpenEditModal(false);
  };

const getData = async () => {
    const response = await getContacts();
    contactsStore.setContacts(response.data);
    setInitialized(true);
  };

const deleteSelectedContact = async id => {
    await deleteContact(id);
    getData();
  };

useEffect(() => {
    if (!initialized) {
      getData();
    }
  });

return (
    <div className="home-page">
      <h1>Contacts</h1>
      <Modal show={openAddModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={false}
            onSave={closeModal.bind(this)}
            onCancelAdd={cancelAddModal}
            contactsStore={contactsStore}
          />
        </Modal.Body>
      </Modal>

<Modal show={openEditModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={true}
            onSave={closeModal.bind(this)}
            contact={selectedContact}
            onCancelEdit={cancelEditModal}
            contactsStore={contactsStore}
          />
        </Modal.Body>
      </Modal>
      <ButtonToolbar onClick={openModal}>
        <Button variant="outline-primary">Add Contact</Button>
      </ButtonToolbar>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Address</th>
            <th>City</th>
            <th>Country</th>
            <th>Postal Code</th>
            <th>Phone</th>
            <th>Email</th>
            <th>Age</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {contactsStore.contacts.map(c => (
            <tr key={c.id}>
              <td>{c.firstName}</td>
              <td>{c.lastName}</td>
              <td>{c.address}</td>
              <td>{c.city}</td>
              <td>{c.country}</td>
              <td>{c.postalCode}</td>
              <td>{c.phone}</td>
              <td>{c.email}</td>
              <td>{c.age}</td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={editContact.bind(this, c)}
                >
                  Edit
                </Button>
              </td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={deleteSelectedContact.bind(this, c.id)}
                >
                  Delete
                </Button>
              </td>
            </tr>
          ))}
        </tbody>
      </Table>
    </div>
  );
}
export default observer(HomePage);

Notice that we have export default observer(HomePage); instead of export default HomePage; . We need to wrap HomePage by the observer function call so that the latest data from the store will be propagated into this component.

It has the table to display the contacts and buttons to add, edit, and delete a contact. It gets data once on the first load with the getData function called in the useEffect’s callback function. useEffect’s callback is called on every render so we want to set a initialized flag and check that it loads only if it’s true.

Note that we pass in all the props in this component to the ContactForm component. To pass an argument a onClick handler function, we have to call bind on the function and pass in the argument for the function as a second argument to bind. For example, in this file, we have editContact.bind(this, c), where c is the contact object. The editContact function is defined as follows:

const editContact = (contact) => {
    setSelectedContact(contact);
    setOpenEditModal(true);
  }

c is the contact parameter we pass in.

Next we create a file called HomePage.css and put:

.home-page {
  padding: 20px;
}

This adds padding to our home page.

In index.js, we replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { ContactsStore } from "./store";
const contactsStore = new ContactsStore();

ReactDOM.render(
  <App contactsStore={contactsStore} />,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: [https://bit.ly/CRA-PWA](https://bit.ly/CRA-PWA)
serviceWorker.unregister();

Then we make a file called requests.js, and add:

const APIURL = '[http://localhost:3000'](http://localhost:3000%27);
const axios = require('axios');
export const getContacts = () => axios.get(`${APIURL}/contacts`);
export const addContact = (data) => axios.post(`${APIURL}/contacts`, data);
export const editContact = (data) => axios.put(`${APIURL}/contacts/${data.id}`, data);
export const deleteContact = (id) => axios.delete(`${APIURL}/contacts/${id}`);

These are functions are making our HTTP requests to the back end to save and delete contacts.

Finally, in public/index.html, we replace the existing code with:

<!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" />
  <link rel="manifest" crossorigin="use-credentials" href="%PUBLIC_URL%/manifest.json" />

<!--
      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/](https://developers.google.com/web/fundamentals/web-app-manifest/)
    -->
  <!--
      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>React Address Book App</title>
  <link rel="stylesheet" href="https://maxcdn.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>

This changes the title and adds the Bootstrap stylesheet.

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.

At the end, we have the following:

Categories
JavaScript Tips

Node.js Tips — Scheduled Tasks, Jest Tests, and Headers for Tests

Like any kind of apps, there are difficult issues to solve when we write Node apps.

In this article, we’ll look at some solutions to common problems when writing Node apps.

Looping Through Dynamic Test Cases with Jest

We can use the tests.each method to loop through each tets.

For instance, we can write:

test.each([[4, 5, 9], [1, 2, 3], [2, 3, 5]])(
  'add(%i, %i)',
  (a, b, expected) => {
    expect(a + b).toBe(expected);
  },
);

The first argument is the items we want to pass into each test.

The nested array entries are the parameters for the callback.

The 2nd argument is the name of each test.

The 3rd is a callback to let the arguments and run our expectations.

Setting Default Headers with Supertest

We can create a header we can use with our test.

Then we can pass it into the set method.

For instance, we can write:

const request = require("supertest");

const baseUrl = 'http://localhost';
request = request(baseUrl);
const commonHeaders = { authorization: "abc" };

describe("testing", () => {
  it.should('present authorization header to server', (done) => {
    request.get('/foo')
      .set(commonHeaders)
      .set({ foo: "bar" })
      .expect(200, done)
  })

})

commonHeaders has the shared headers between multiple tests.

We just pass that into the set method.

Then we can pass other headers into the set method that are specific to the test.

Read a File Synchronously in Node.js

To read a file synchronously with Node, we can use the readFileSync method.

For instance, we can write:

const fs = require('fs');
const content = fs.readFileSync('file');
console.log(content);

We just pass in the path to the file and the content will be returned.

Base64 Encode a JavaScript Object

We can base64 encode a JavaScript object by converting it to a string with JSON.stringigy .

Then we can call Buffer.from to convert it to a buffer and then call toString to convert it to a base64 string.

For instance, we can write:

const str = JSON.stringify(obj);
const objJsonB64 = Buffer.from(str).toString("base64");

Buffer.from converts the stringified JSON object to a byte stream.

Then we call toString with the 'base64' argument to convert it to a base64 string.

Mongoose Populate Embedded

We can call populate on embedded documents with Mongoose if we define a schema with the related schemas.

For instance, if we have:

const UserSchema = new Schema({
  name: String,
  friends: [{ type: ObjectId, ref: 'User' }]
});

Then we have a self-referencing schema since friends is referencing another User document.

Now we can call populate with friends by writing:

User.
  findOne({ name: 'james' }).
  populate({
    path: 'friends',
    populate: { path: 'friends' }
  });

We call populate the friends to get friends.

And we can use the populate property to get friends of friends.

Running a Function Everyday

We can run a function in a scheduled manner by using the node-schedule package.

For instance, we can write:

import schedule from 'node-schedule'

schedule.scheduleJob('0 0 0 * * *', () => {
  //...
})

We run the schedule.scheduleJob to run the callback with the scheduled specified by the string.

It takes a string in that specifies the schedule in the same way as a cron job.

The first number is the second.

The 2nd is the minute.

The 3rd is the hour.

The 4th is the day of the month.

The 5th is the month.

The 6th is the day of the week.

We specify the hour, minute, and second to specify when it’ll run in the day.

Then the rest are asterisks to that it’ll run every day.

We can also specify the date instead of a cron format string.

For instance, we can write:

const schedule = require('node-schedule');
const date = new Date(2020, 0, 0, 0, 0, 0);

const j = schedule.scheduleJob(date, () => {
  console.log('hello');
});

Then the callback only runs on the given date.

We can also set recurrence rules.

For instance, we can write:

const schedule = require('node-schedule');

const rule = new schedule.RecurrenceRule();
rule.minute = 30;

const j = schedule.scheduleJob(rule, () => {
  console.log('half hour job');
});

We use the schedule.RecurrenceRule constructor to create a rule.

We set the minute in the example above.

But we can also set the second , hour , date , month , year , and dayOfWeek .

Conclusion

node-schedule is a useful scheduler package.

We can set headers for tests with supertest.

Jest can define and run tests dynamically.

MongoDB can reference related documents.

We can convert JavaScript objects into base64 strings.

Categories
JavaScript Tips

JavaScript Tips — Jest Mocks, React Background, and More

Like any kind of apps, there are difficult issues to solve when we write JavaScript apps.

In this article, we’ll look at some solutions to common JavaScript problems.

Regex to Check Whether a String Contains Only Numbers

We can use a regex to check if a string has only numbers.

For instance, we can write:

const reg = /^d+$/;

^ matches the string from the beginning, and d is the digit.

Javascript Filename Naming Convention

JavaScript filename should follow some common conventions.

We should use all lowercase filenames.

Also, we don’t want to use spaces in the filename.

We can optionally use a hyphen as a word separator.

And we may also add a version number in our file name if we have one.

By updating the version number in the file name, we always serve the latest one with the CDN alongside the older version.

Calling a Parent Window Function from an iframe

We can access the parent window with the window.parent property.

So if we want to call a function in the parent window from an iframe, we can write:

<a onclick="parent.foo();" href="#" >click me</a>

foo is a top-level function in the parent window.

Center a Popup Window on the Screen

To center a popup window on the screen, we can do some calculations to center the window and pass that off to the specification string.

For instance, we can write:

const openPopup = (url, title, w, h) => {
  const left = (screen.width / 2) - (w / 2);
  const top = (screen.height / 2) - (h / 2);
  return window.open(url, title, `width=${w}, height=${h}, top=${top}, left=${top}`);
}

We get the left coordinate by using the width divided by 2 minus the width divided by 2 to get the x coordinate of the top left corner.

Likewise, we do the same thing but replace the width with the height to get the y coordinate of the top left corner.

Then we pass those to the specification string and also do the same with the width and height.

Setting a Background Image With React Inline Styles

To set a background image with React inline styles, we can import the image as a module.

Then we can interpolate that into the CSS string for setting the background image.

For instance, we can write:

import Background from '../images/background.png';

const styles = {
  backgroundImage: `url(${Background})`
}

We set the backgroundImage property with the url string with the Background image we imported.

Remove Everything After a Given Character

To remove the part of a string after a given character from a string, we can use the split method.

For instance, we can write:

url.split('?')[0]

Then we get part of the url string before the ? .

A Practical Use for a Closure in JavaScript

Closures are great for hiding private things like variables and functions that we don’t want to expose to the outside.

For instance, we can write:

const obj = (() => {
  const private = () => {
    console.log('hello');
  }

  return {
    public() {
      private();
    }
  }
})();

private is a private function.

And public is a function that’s in obj , so it’s available to code outside.

Get the Real Width and Height of an Image

To get the real width and height of an image, we can use the naturalHeight property to get the real height and naturalWidth to get the real width.

For instance, we can write:

const height = document.querySelector('img').naturalHeight;

and:

const width = document.querySelector('img').naturalWidth;

This is available since HTML5.

Mock an ES6 Module Import Using Jest

To mock an ES6 module import using Jest, we can use the jest.fn() function to mock a function.

For instance, we can write:

logger.js

export const log = (y) => console.log(y)

module.js

import { log } from './logger';

export default (x) => {
  doSomething(x * 2);
}

module.test.js

import module from '../module';
import * as `logger` from '../`logger`';

describe('module test', () => {
  it('logs the double the input', () => {
    dependency.`log` = jest.fn();
    module(2);
    expect(dependency.`log`).toBeCalledWith(4);
  });
});

All we have to do is to call jest.fn to create a mock function.

We imported the whole module, so the members aren’t read-only.

Therefore, we can assign our own mock function to it.

Then we call our module function to test our function and check our results.

Conclusion

We should follow some common file naming conventions for naming JavaScript files.

Also, we can mock functions in Jest with the jest.fn() method.

A popup can be centered on the screen.

We can set a background image by importing it and interpolating it in a CSS string in a React component.

Categories
Express Nodejs Testing

Add Tests to Express Apps With Jest and SuperTest

Automated tests are essential to the apps we write since modern apps have so many moving parts.

In this piece, we’ll look at how to write apps to test an Express app that interacts with a database with Jest and SuperTest.


Creating the App We’ll Test

We create a project folder by creating an empty folder and running the following to create a package.json file with the default answers:

npm init -y

Then we run the following to install the packages for our apps:

npm i express sqlite3 body-parser

Then, we create the app.js file for our app and write:

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');
const app = express();
const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;
let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

db.serialize(() => {
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
});

app.use(bodyParser.json());
app.get('/', (req, res) => {
    db.serialize(() => {
        db.all('SELECT * FROM persons', [], (err, rows) => {
            res.json(rows);
        });
    })
})

app.post('/', (req, res) => {
    const { name, age } = req.body;
    db.serialize(() => {
        const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');
        stmt.run(name, age);
        stmt.finalize();
        res.json(req.body);
    })
})

app.put('/:id', (req, res) => {
    const { name, age } = req.body;
    const { id } = req.params;
    db.serialize(() => {
        const stmt = db.prepare('UPDATE persons SET name = ?, age = ? WHERE id = ?');
        stmt.run(name, age, id);
        stmt.finalize();
        res.json(req.body);
    })
})

app.delete('/:id', (req, res) => {
    const { id } = req.params;
    db.serialize(() => {
        const stmt = db.prepare('DELETE FROM persons WHERE id = ?');
        stmt.run(id);
        stmt.finalize();
        res.json(req.body);
    })
})

const server = app.listen(port);
module.exports = { app, server };

The code above has the app we’ll test.

To make our app easier to test, we have:

const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;
let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

So we can set the process.env.NODE_ENV to 'test' to make our app listen to a different port than it does when the app is running in a nontest environment.

We’ll use the 'test' environment to run our tests.

Likewise, we want our app to use a different database when running unit tests than when we aren’t running them.

This is why we have:

let db;
if (process.env.NODE_ENV === 'test') {
    db = new sqlite3.Database(':memory:');
}
else {
    db = new sqlite3.Database('db.sqlite');
}

We specified that when the app is running in a 'test' environment we want to use SQLite’s in-memory database rather than a database file.


Writing the Tests

Initialization the code

With the app made to be testable, we can add tests to it.

We’ll use the Jest test runner and SuperTest to make requests to our routes in our tests. To add Jest and SuperTest, we run:

npm i jest supertest

Then, we add app.test.js to the same folder as the app.js file we had above.

In app.test.js, we start by writing the following:

const { app } = require('./app');
const sqlite3 = require('sqlite3').verbose();
const request = require('supertest');
const db = new sqlite3.Database(':memory:');

beforeAll(() => {
    process.env.NODE_ENV = 'test';
})

In the code above, we included our Express app from our app.js. Then we also included the SQLite3 and SuperTest packages.,

Then, we connected to our in-memory database with:

const db = new sqlite3.Database(':memory:');

Next, we set all the tests to run in the 'test' environment by running:

beforeAll(() => {
    process.env.NODE_ENV = 'test';
})

This will make sure we use port 3001 and the in-memory database we specified in app.js for each test.

To make our tests run independently and with consistent results, we have to clean our database and insert fresh data every time.

To do this, we create a function we call on each test:

const seedDb = db => {
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
    db.run('DELETE FROM persons');
    const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');
    stmt.run('Jane', 1);
    stmt.finalize();
}

The code above creates the persons table if it doesn’t exist and deletes everything from there afterward.

Then we insert a new value in there to have some starting data.


Adding Tests

With the initialization code complete, we can write the tests.

GET request test

First, we write a test to get the existing seed data from the database with a GET request.

We do this by writing:

test('get persons', () => {
    db.serialize(async () => {
        seedDb(db);
        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

We put everything inside the callback of db.serialize so the queries will be run sequentially.

First, we call seedDb, which we created above to create the table if it doesn’t exist, to clear out the database, and to add new data.

Then, we call the GET request by writing:

await request(app).get('/');

This gets us the res object with the response resolved from the promise.

request(app) will start the Express app so we can make the request.

Next, we have the response for us to check against for correctness:

const response = [
  { name: 'Jane', id: 1, age: 1 }
]

Then, we check the responses to see if we get what we expect:

expect(res.status).toBe(200);
expect(res.body).toEqual(response);

The toBe method checks for shallow equality, and toEqual checks for deep equality. So we use toEqual to check if the whole object structure is the same.

res.status checks the status code returned from the server, and res.body has the response body.

POST request test

Next, we add a test for the POST request. It’s similar to the GET request test.

We write the following code:

test('add person', () => {
    db.serialize(async () => {
        seedDb(db);
        await request(app)
            .post('/')
            .send({ name: 'Joe', age: 2 });

        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 },
            { name: 'Joe', id: 2, age: 2 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

First, we reset the database with:

seedDb(db);

We made our POST request with:

await request(app)
  .post('/')
  .send({ name: 'Joe', age: 2 });

This will insert a new entry into the in-memory database.

Finally, to check for correctness, we make the GET request — like in our first test — and check if both entries are returned:

const res = await request(app).get('/');
const response = [
  { name: 'Jane', id: 1, age: 1 },
  { name: 'Joe', id: 2, age: 2 }
]
expect(res.status).toBe(200);
expect(res.body).toEqual(response);

PUT and DELETE tests

The test for the PUT request is similar to the POST request. We reset the database, make the PUT request with our payload, and then make the GET request to get the returned data, as follows:

test('update person', () => {
    db.serialize(async () => {
        seedDb(db);
        await request(app)
            .put('/1')
            .send({ name: 'Joe', age: 2 });

        const res = await request(app).get('/');
        const response = [
            { name: 'Jane', id: 1, age: 1 }
        ]
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

Then we can replace the PUT request with the DELETE request and test the DELETE request:

test('delete person', () => {
    db.serialize(async () => {
        seedDb(db);
        const res = await request(app).delete('/1');
        const response = [];
        expect(res.status).toBe(200);
        expect(res.body).toEqual(response);
    })
});

Running the Tests

To run the tests, we add the following to the scripts section:

"test": "jest --forceExit"

We have to add the --forceExit option so Jest will exist after the tests are run. There’s no fix for the issue where Jest tests using SuperTest don’t exit properly yet.

Then we run the following to run the tests:

npm test

And we should get:

PASS  ./app.test.js
  √ get persons (11ms)
  √ add person (2ms)
  √ update person (2ms)
  √ delete person (6ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.559s
Ran all test suites.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

We should get the same thing no matter how many times we run the tests since we reset the database and made all database queries run sequentially.

Also, we used a different database and port for our tests than other environments, so the data should be clean.


Conclusion

We can add tests run with the Jest test runner. To do this, we have to have a different port and database for running the tests. Then we create the tables if they don’t already exist, clear all the data, and add seed data so we have the same database structure and content for every test.

With SuperTest, we can run the Express app automatically and make the request we want. Then, we can check the output.

Categories
Testing

Running Repetitive Test Code with Jest

In our unit tests, we often have to write test code that runs before and after each test. We’ve to write them often to run code to set up fixtures before tests and run code to clean up everything after tests.

We can easily do this with Jest since it comes with a few hooks to do this.

In this article, we’ll look at how to write repetitive setup and teardown code in a way that isn’t repetitive.

How to Write Setup and Teardown Code

With Jest, we can write setup and teardown code by using the beforeEach and afterEach hooks.

beforeEach runs code before each test, and afterEach runs code after each test. The order applies inside a describe block and if there’s no describe block, for the whole file.

For example, given that we have the following code in example.js :

const getStorage = (key) => localStorage.getItem(key);
const setStorage = (key, value) => localStorage.setItem(key, value);
module.exports = {
    getStorage,
    setStorage
}

We write the test for them as follows:

const { getStorage, setStorage } = require('./example');

beforeEach(() => {
    localStorage.setItem('foo', 1);
    expect(localStorage.getItem('bar')).toBeNull();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

We pass a callback function into the beforeEach hook to run code before each test that we have below.

In this example, we prepopulate local storage with an item with key 'foo' and value '1' .

We also check that local storage doesn’t have the item with key 'bar’ with:

expect(localStorage.getItem('bar')).toBeNull();

All tests will pass with the expect we have above since we’ll run localStorage.clear() in the afterEach hook.

Likewise, we pass in a callback to afterEach to run code after each test is run.

We clear local storage with:

localStorage.clear();

Then when running the tests, we get that item with key'bar' is null since we didn’t populate it in the first test.

In the second test, we’ll get that expect(getStorage(‘foo’)).toBe(‘1’); passing since we populated the local storage with it in our beforeEach hook.

Then since we ran:

setStorage('bar', 2);

to to save an item with key 'bar' and value '2' , we’ll get that:

expect(bar).toBe(2);

passing since we saved the item in the test.

Asynchronous Code

In the example above, we ran synchronous code in our hooks. We can also run asynchronous code in our hooks if they either take a done parameter or return a promise.

If we want to run a function that takes a callback and runs it asynchronously as follows:

const asyncFn = (callback) => {
    setTimeout(callback, 500);
}

Then in the beforeEach hook we can run asyncFn as follows:

const { asyncFn } = require('./example');

beforeEach((done) => {
    const callback = () => {
        localStorage.setItem('foo', 1);
        done();
    }
    asyncFn(callback);
    expect(localStorage.getItem('bar')).toBeNull();
});

It does the same thing as the previous beforeEach callback except it’s done asynchronously. Note that we call done passed in from the parameter in our callback.

If we omit, it, the tests will fail as they time out. The code in the tests is the same as before.

We can wait for promises to resolve by returning it in the callback we pass into the hooks.

For example, in example.js , we can write the following function to run a promise:

const promiseFn = () => {
    return new Promise((resolve) => {
        localStorage.setItem('foo', 1);
        resolve();
    });
}

Then we can put promiseFn in module.exports and then run it in our beforeEach by running:

beforeEach(() => {
    expect(localStorage.getItem('bar')).toBeNull();
    return promiseFn();
});

We ran promiseFn which we imported before this hook and we return the promise returned by that function, which sets the local storage like the first example except it’s done asynchronously.

Then after we put everything together, we have the following code in example.js , which we run in our test code in the hooks and for testing:

const getStorage = (key) => localStorage.getItem(key);
const setStorage = (key, value) => localStorage.setItem(key, value);
const asyncFn = (callback) => {
    setTimeout(callback, 500);
}
const promiseFn = () => {
    return new Promise((resolve) => {
        localStorage.setItem('foo', 1);
        resolve();
    });
}
module.exports = {
    getStorage,
    setStorage,
    asyncFn,
    promiseFn
}

Then we have asyncExample.test.js to change the hook to be asynchronous:

const { getStorage, setStorage, asyncFn } = require('./example');

beforeEach((done) => {
    const callback = () => {
        localStorage.setItem('foo', 1);
        done();
    }
    asyncFn(callback);
    expect(localStorage.getItem('bar')).toBeNull();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

Then in example.test.js we have:

const { getStorage, setStorage } = require('./example');

beforeEach(() => {
    localStorage.setItem('foo', 1);
    expect(localStorage.getItem('bar')).toBeNull();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

and finally in promiseExample.test.js we have:

const { getStorage, setStorage, promiseFn } = require('./example');

beforeEach(() => {
    expect(localStorage.getItem('bar')).toBeNull();
    return promiseFn();
});

afterEach(() => {
    localStorage.clear();
});

test('getStorage("foo") is 1', () => {
    expect(getStorage('foo')).toBe('1');
});

test('setStorage saves data to local storage', () => {
    setStorage('bar', 2);
    const bar = +localStorage.getItem('bar');
    expect(getStorage('foo')).toBe('1');
    expect(bar).toBe(2);
});

BeforeAll and AfterAll

To only run the setup and teardown code once in each file, we can use the beforeAll and afterAll hooks. We can pass in a callback with the code that we want to run like we did with the beforeEach and afterEach hooks.

The only difference is that the callbacks are run once before the test in a file is run and after the test in a file is run instead of running them before and after each test.

Callback of beforeAll runs after the callback for beforeEach and callback for afterAll runs after the callback forafterEach .

The order applies inside a describe block and if there’s no describe block, for the whole file.

Running Only One Test

We can write test.only instead of test to run only one test. This is handy for troubleshooting since we don’t have to run all the tests and it helps us pin issues with the test that’s failing.

To write test code that needs to be run for all tests, we use the beforeEach and afterEach hooks in Jest.

To write test code that’s only run per describe block or file, we can use the beforeAll and afterAll hooks.

Callback of beforeAll runs after the callback for beforeEach and callback for afterAll runs after the callback forafterEach .