REST API using AWS SAM+ AMPLIFY - Part 5
Tags: aws, tutorial, awssam, serverless
Published: 2023-08-20 06:59:57

# Modifying the Front-end
We will modify our react frontend using Ant Design tables and forms. We will also use Axios to fetch data from our API endpoint. 


**Step 1.** Inside the api folder, we will create a file and name it "apiEndPoints.ts". This file will contain our API endpoints from our API Gateway. 

**apiEndPoints.ts**

```javascript
import axios from "axios";
import { Item } from "../components/Home";

const apiInventory = `${process.env.REACT_APP_ENDPOINT}inventory`; //link from the .env file

const apiHealthCheck = `${process.env.REACT_APP_ENDPOINT}check`; //link from the .env file

const apiCars = `${process.env.REACT_APP_ENDPOINT}car`; //link from the .env file

//function to generate random ID:
const id = () => {
  return Math.random().toString(36).substring(2) + Date.now().toString(36);
};

export const getInventory = () => {
  axios
    .get(apiInventory)
    .then((response) => {
      return response.data.inventory;
    })
    .catch((error) => {
      console.log(error);
    });
};

export const healthCheck = () => {
  axios
    .get(apiHealthCheck)
    .then((response) => {
      console.log(response);
    })
    .catch((error) => {
      console.log(error);
    });
};

export const getCar = () => {
  axios
    .get(apiCars)
    .then((response) => {
      console.log(response);
    })
    .catch((error) => {
      console.log(error);
    });
};

export const postCar = (value: Item) => {
  axios
    .post(
      apiCars,
      {
        model: value.model,
        maker: value.maker,
        engineCyl: value.engineCyl,
        rating: value.rating,
        mpgHighway: value.mpgHighway,
        year: value.year,
        id: id(),
        mpgCity: value.mpgCity,
        mpgCombined: value.mpgCombined,
        engineSize: value.engineSize,
      },
      {
        headers: {
          "Content-Type": "application/json",
        },
      }
    )
    .then((response) => {
      console.log(response.data);
    })
    .catch((error) => {
      console.log(error);
    });
};

export const putCar = (value: Item) => {
  axios
    .post(
      apiCars,
      {
        model: value.model,
        maker: value.maker,
        engineCyl: value.engineCyl,
        rating: value.rating,
        mpgHighway: value.mpgHighway,
        year: value.year,
        id: value.id,
        mpgCity: value.mpgCity,
        mpgCombined: value.mpgCombined,
        engineSize: value.engineSize,
      },
      {
        headers: {
          "Content-Type": "application/json",
        },
      }
    )
    .then((response) => {
      console.log(response.data);
    })
    .catch((error) => {
      console.log(error);
    });
};

export const deleteCar = async (id: string) => {
  const data = JSON.stringify({
    id: id,
  });

  const config = {
    method: "delete",
    url: apiCars,
    headers: {
      "Content-Type": "application/json",
    },
    data: data,
  };

  axios(config)
    .then(function (response) {
      console.log(JSON.stringify(response.data));
    })
    .catch(function (error) {
      console.log(error);
    });
};

```

---


**Understanding "apiEndPoints.ts":**

```javascript
import axios from "axios";
import { Item } from "../components/Home";
```

- In the provided code, two main modules are imported: the axios library, which is widely recognized for facilitating HTTP requests in JavaScript, and a type named Item, which is an interface or a type definition sourced from a component within the Home directory. 

---


```javascript

const apiInventory = `${process.env.REACT_APP_ENDPOINT}inventory`;
const apiHealthCheck = `${process.env.REACT_APP_ENDPOINT}check`;
const apiCars = `${process.env.REACT_APP_ENDPOINT}car`;

```

- The code establishes certain constants such as apiInventory, apiHealthCheck, and apiCars, which are essentially API endpoints. These are derived by merging a fundamental URL obtained from the environment variable named REACT_APP_ENDPOINT with distinct path segments specific to each API function like 'inventory', 'check', and 'car'. 

---


```javascript

const id = () => {
  return Math.random().toString(36).substring(2) + Date.now().toString(36);
};

```

-  The function named id, devised to generate _unique IDs_. It achieves this by amalgamating a randomly generated string (originating from a number) with the present timestamp.

---



- The rest of the code outlines several API request functions. The _getInventory_ function fetches car inventory using a GET request and logs errors. _healthCheck_ performs a basic health check and logs the outcome. _getCar_ retrieves car details and logs the response. _postCar_ adds a new car to the inventory, including a new ID, using a POST request._putCar_ uses a POST method to update car details. Lastly, _deleteCar_ removes a car using its ID with a DELETE request, and any outcomes or issues are logged.

---

**Step 2.** Within the components directory, we will generate a file named Home.tsx. This file is intended to function as the landing page for our application. For the presentation of tables and forms, we will employ the Ant Design library


**Home.tsx**
```javascript
import React, { useState, useEffect } from "react";
import {
  Form,
  Input,
  InputNumber,
  Popconfirm,
  Table,
  Typography,
} from "antd";
import axios from "axios";
import { putCar, deleteCar } from "../api/apiEndPoint";
import FormComponent from "./FormComponent";


export interface Item {
  id: string;
  engineCyl: string;
  engineSize: string;
  maker: string;
  model: string;
  mpgCity: string;
  mpgCombined: string;
  mpgHighway: string;
  rating: string;
  year: string;
}

interface EditableCellProps extends React.HTMLAttributes<HTMLElement> {
  editing: boolean;
  dataIndex: string;
  title: string;
  inputType: "number" | "text";
  record: Item;
  index: number;
  children: React.ReactNode;
}

const { Title } = Typography; //Typography for Ant Design

const EditableCell: React.FC<EditableCellProps> = ({
  editing,
  dataIndex,
  title,
  inputType,
  record,
  index,
  children,
  ...restProps
}) => {
  const inputNode = inputType === "number" ? <InputNumber /> : <Input />;

  return (
    <td {...restProps}>
      {editing ? (
        <Form.Item
          name={dataIndex}
          style={{ margin: 0 }}
          rules={[
            {
              required: true,
              message: `Please Input ${title}!`,
            },
          ]}
        >
          {inputNode}
        </Form.Item>
      ) : (
        children
      )}
    </td>
  );
};

const Home: React.FC = () => {
  const [data, setData] = useState<Item[]>([]);

  useEffect(() => {
    const api = `${process.env.REACT_APP_ENDPOINT}inventory`; //link from the .env file
    axios
      .get(api)
      .then((response) => {
        const dataWithKeys = response.data.inventory.map(
          (item: { id: string }) => ({ ...item, key: item.id })
        );
        setData(dataWithKeys);
        console.log(response);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);

  const [form] = Form.useForm();
  const [editingKey, setEditingKey] = useState("");
  const isEditing = (record: Item) => record.id === editingKey;

  
  const edit = (record: Partial<Item> & { id: React.Key }) => {
    form.setFieldsValue({
      engineCyl: "",
      engineSize: "",
      maker: "",
      model: "",
      mpgCity: "",
      mpgCombined: "",
      mpgHighway: "",
      rating: "",
      year: "",
      ...record,
    });
    setEditingKey(record.id);
  };

  const cancel = () => {
    setEditingKey("");
  };

  const save = async (key: React.Key) => {
    try {
      const row = (await form.validateFields()) as Item;
      const newData = [...data];
      const index = newData.findIndex((item) => key === item.id);
      if (index > -1) {
        const item = newData[index];
        newData.splice(index, 1, { ...item, ...row });
        setData(newData);
        console.log(newData[index]);
        putCar(newData[index]);
        setEditingKey("");
      } else {
        newData.push(row);
        setData(newData);
        putCar(newData[index]);
        console.log(newData);
        setEditingKey("");
      }
    } catch (errInfo) {
      console.log("Validate Failed:", errInfo);
    }
  };

  const handleDelete = (key: React.Key) => {
    const dataSource = [...data];
    const newData = dataSource.filter((item) => item.id !== key);
    setData(newData);
  };

  const columns = [
    {
      title: "model",
      dataIndex: "model",
      width: "10%",
      editable: true,
    },
    {
      title: "maker",
      dataIndex: "maker",
      width: "10%",
      editable: true,
    },
    {
      title: "year",
      dataIndex: "year",
      width: "10%",
      editable: true,
    },
    {
      title: "engineCyl",
      dataIndex: "engineCyl",
      width: "10%",
      editable: true,
    },
    {
      title: "engine size",
      dataIndex: "engineSize",
      width: "10%",
      editable: true,
    },
    {
      title: "mpgCity",
      dataIndex: "mpgCity",
      width: "10%",
      editable: true,
    },
    {
      title: "mpgHighway",
      dataIndex: "mpgHighway",
      width: "10%",
      editable: true,
    },
    {
      title: "mpgCombined",
      dataIndex: "mpgCombined",
      width: "10%",
      editable: true,
    },
    {
      title: "rating 1-10",
      dataIndex: "rating",
      width: "5%",
      editable: true,
    },
    {
      title: "operation",
      dataIndex: "operation",
      render: (_: unknown, record: Item) => {
        const editable = isEditing(record);
        return editable ? (
          <span>
            <Typography.Link
              onClick={() => save(record.id)}
              style={{ marginRight: 8 }}
            >
              Save
            </Typography.Link>
            <Typography.Link onClick={cancel} style={{ marginRight: 8 }}>
              Cancel
            </Typography.Link>
            <Popconfirm
              title="Sure to delete?"
              onConfirm={() => {
                deleteCar(record.id);
                handleDelete(record.id);
              }}
            >
              <a>Delete</a>
            </Popconfirm>
          </span>
        ) : (
          <Typography.Link
            disabled={editingKey !== ""}
            onClick={() => edit(record)}
          >
            Edit
          </Typography.Link>
        );
      },
    },
  ];

  const mergedColumns = columns.map((col) => {
    if (!col.editable) {
      return col;
    }
    return {
      ...col,
      onCell: (record: Item) => ({
        record,
        inputType: col.dataIndex === "none" ? "number" : "text",
        dataIndex: col.dataIndex,
        title: col.title,
        editing: isEditing(record),
      }),
    };
  });

  return (
    <>
      <Title level={1}> Amplify + AWS SAM Application</Title>
      <Form form={form} component={false}>
        <Table
          components={{
            body: {
              cell: EditableCell,
            },
          }}
          bordered
          dataSource={data}
          columns={mergedColumns}
          rowClassName="editable-row"
          pagination={{
            onChange: cancel,
          }}
        />
      </Form>
      <FormComponent />
    </>
  );
};

export default Home;

```

---


**Understanding "Home.tsx":**

The main goal of this component is to provide an interface for viewing a table of car data and facilitates editing, saving, and deleting entries, communicating changes to the backend.

Starting off, various dependencies are imported. Notably:

```javascript

import React, { useState, useEffect } from "react";
import {
  Form,
  Input,
  InputNumber,
  Popconfirm,
  Table,
  Typography,
} from "antd";
import axios from "axios";
import { putCar, deleteCar } from "../api/apiEndPoint";
import FormComponent from "./FormComponent";

```


- Core React functionalities **useState** and **useEffect** for state management and side effects.

- UI components and utilities from the **Ant Design library (Form, Input, Table, and so on)**.

- An HTTP client, **axios**, for making server requests.

- API functions, **putCar** and **deleteCar**, for updating and deleting cars, respectively.


---


```javascript
export interface Item {
  id: string;
  engineCyl: string;
  engineSize: string;
  maker: string;
  model: string;
  mpgCity: string;
  mpgCombined: string;
  mpgHighway: string;
  rating: string;
  year: string;
}

interface EditableCellProps extends React.HTMLAttributes<HTMLElement> {
  editing: boolean;
  dataIndex: string;
  title: string;
  inputType: "number" | "text";
  record: Item;
  index: number;
  children: React.ReactNode;
}
```

The code defines an interface _Item_ that outlines the structure of a car's data. Also an interface _EditableCellProps_ that details the properties for an editable cell within the table.

---


```javascript
const EditableCell: React.FC<EditableCellProps> = ({
  editing,
  dataIndex,
  title,
  inputType,
  record,
  index,
  children,
  ...restProps
}) => {
  const inputNode = inputType === "number" ? <InputNumber /> : <Input />;

  return (
    <td {...restProps}>
      {editing ? (
        <Form.Item
          name={dataIndex}
          style={{ margin: 0 }}
          rules={[
            {
              required: true,
              message: `Please Input ${title}!`,
            },
          ]}
        >
          {inputNode}
        </Form.Item>
      ) : (
        children
      )}
    </td>
  );
};
```

Function **EditableCell** renders a table cell, which can be either in an editing state (displaying an input field) and/or a default state (displaying the cell's value).

---


On the main component, Home, maintains the state for the table data (data), the form (form), and the currently edited table row (editingKey).

---


```javascript
useEffect(() => {
    const api = `${process.env.REACT_APP_ENDPOINT}inventory`; //link from the .env file
    axios
      .get(api)
      .then((response) => {
        const dataWithKeys = response.data.inventory.map(
          (item: { id: string }) => ({ ...item, key: item.id })
        );
        setData(dataWithKeys);
        console.log(response);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);
```

On the initial component render, a **useEffect** hook triggers a call to fetch the car inventory from an API endpoint, appending unique keys for each data row.

---


```javascript

  const edit = (record: Partial<Item> & { id: React.Key }) => {
    form.setFieldsValue({
      engineCyl: "",
      engineSize: "",
      maker: "",
      model: "",
      mpgCity: "",
      mpgCombined: "",
      mpgHighway: "",
      rating: "",
      year: "",
      ...record,
    });
    setEditingKey(record.id);
  };

  const cancel = () => {
    setEditingKey("");
  };

  const save = async (key: React.Key) => {
    try {
      const row = (await form.validateFields()) as Item;
      const newData = [...data];
      const index = newData.findIndex((item) => key === item.id);
      if (index > -1) {
        const item = newData[index];
        newData.splice(index, 1, { ...item, ...row });
        setData(newData);
        console.log(newData[index]);
        putCar(newData[index]);
        setEditingKey("");
      } else {
        newData.push(row);
        setData(newData);
        putCar(newData[index]);
        console.log(newData);
        setEditingKey("");
      }
    } catch (errInfo) {
      console.log("Validate Failed:", errInfo);
    }
  };

  const handleDelete = (key: React.Key) => {
    const dataSource = [...data];
    const newData = dataSource.filter((item) => item.id !== key);
    setData(newData);
  };
```

Functions within Home to facilitate editing:

- **edit** prepares the form for editing a specific table row.
- **cancel** resets the editing state.
- **save** validates the changes made in the form, updates the local data state, sends an update to the backend using **putCar**, and resets the editing state.
- **handleDelete** removes a car from the local state and invokes the **deleteCar** function to delete the car from the backend.


---

```javascript
 const columns = [
    {
      title: "model",
      dataIndex: "model",
      width: "10%",
      editable: true,
    },
    {
      title: "maker",
      dataIndex: "maker",
      width: "10%",
      editable: true,
    },
    {
      title: "year",
      dataIndex: "year",
      width: "10%",
      editable: true,
    },
    {
      title: "engineCyl",
      dataIndex: "engineCyl",
      width: "10%",
      editable: true,
    },
    {
      title: "engine size",
      dataIndex: "engineSize",
      width: "10%",
      editable: true,
    },
    {
      title: "mpgCity",
      dataIndex: "mpgCity",
      width: "10%",
      editable: true,
    },
    {
      title: "mpgHighway",
      dataIndex: "mpgHighway",
      width: "10%",
      editable: true,
    },
    {
      title: "mpgCombined",
      dataIndex: "mpgCombined",
      width: "10%",
      editable: true,
    },
    {
      title: "rating 1-10",
      dataIndex: "rating",
      width: "5%",
      editable: true,
    },
    {
      title: "operation",
      dataIndex: "operation",
      render: (_: unknown, record: Item) => {
        const editable = isEditing(record);
        return editable ? (
          <span>
            <Typography.Link
              onClick={() => save(record.id)}
              style={{ marginRight: 8 }}
            >
              Save
            </Typography.Link>
            <Typography.Link onClick={cancel} style={{ marginRight: 8 }}>
              Cancel
            </Typography.Link>
            <Popconfirm
              title="Sure to delete?"
              onConfirm={() => {
                deleteCar(record.id);
                handleDelete(record.id);
              }}
            >
              <a>Delete</a>
            </Popconfirm>
          </span>
        ) : (
          <Typography.Link
            disabled={editingKey !== ""}
            onClick={() => edit(record)}
          >
            Edit
          </Typography.Link>
        );
      },
    },
  ];

  const mergedColumns = columns.map((col) => {
    if (!col.editable) {
      return col;
    }
    return {
      ...col,
      onCell: (record: Item) => ({
        record,
        inputType: col.dataIndex === "none" ? "number" : "text",
        dataIndex: col.dataIndex,
        title: col.title,
        editing: isEditing(record),
      }),
    };
  });
```

**columns** (see [Ant Design Table documentations](https://ant.design/components/table)): The table's columns configuration, columns, describes how each data column should be displayed. Columns include details like "model", "maker", "year", and others. Additionally, we will create an "operation" column that provides Edit, Save, Cancel, and Delete functionalities. When a row is being edited, options to save or cancel the edit (and delete the row) are shown. Otherwise, the row displays an option to start editing.

Additionally, **mergedColumns** processes the original columns configuration, adding **onCell** properties to editable columns.

---

```javascript
return (
    <>
      <Title level={1}> Amplify + AWS SAM Application</Title>
      <Form form={form} component={false}>
        <Table
          components={{
            body: {
              cell: EditableCell,
            },
          }}
          bordered
          dataSource={data}
          columns={mergedColumns}
          rowClassName="editable-row"
          pagination={{
            onChange: cancel,
          }}
        />
      </Form>
      <FormComponent />
    </>
  );
};
```

- The component's render function returns the table UI, with the **EditableCell** component specified for rendering the table's cells. Below the table, the **FormComponent** is also rendered. The table title reads "Amplify + AWS SAM Application". 

---

**STEP 3.** Inside the components folder, we will create a file named "FormComponents.tsx". The purpose of this file is to provide a user interface, to submit car-related data using a form. Utilizing Ant Design's components, the form will capture various _car attributes, such as model, maker, year, engine cylinder, engine size, miles per gallon (MPG) for city, highway, and combined, as well as a rating._ When a user submits the form, the **onFinish** function attempts to post this data using the **postCar ** function from an API module. If the submission is successful, the form fields are reset, and a success notification is displayed to the user. If there's an error during the submission process, a warning notification alerts the user. _The form's layout can be adjusted through state management, and it comes with predefined styles and validation rules ensuring that all fields are filled out before submission._

**FormComponent.tsx**

```javascript

import { Button, Form, Input, notification } from "antd";
import React, { useState } from "react";
import { postCar } from "../api/apiEndPoint";

type LayoutType = Parameters<typeof Form>[0]["layout"];
type NotificationType = "success" | "info" | "warning" | "error";

interface FormValueProps {
  model: string;
  maker: string;
  engineCyl: string;
  rating: string;
  mpgHighway: string;
  year: string;
  id: string;
  mpgCity: string;
  mpgCombined: string;
  engineSize: string;
}

const fields = [
  { label: "Car Model", name: "model" },
  { label: "Car Maker", name: "maker" },
  { label: "Year", name: "year" },
  { label: "Engine Cylinder", name: "engineCyl" },
  { label: "Engine Size", name: "engineSize" },
  { label: "MPG City", name: "mpgCity" },
  { label: "MPG Highway", name: "mpgHighway" },
  { label: "MPG Combined", name: "mpgCombined" },
  { label: "Rating", name: "rating" },
];

const FormComponent: React.FC = () => {
  const [form] = Form.useForm();
  const [formLayout, setFormLayout] = useState<LayoutType>("horizontal");
  const [api, contextHolder] = notification.useNotification();

  const openNotificationWithIcon = (type: NotificationType) => {
    api[type]({
      message: type === "success" ? "Notification" : "Alert",
      description:
        type === "success"
          ? "The form is submitted successfully!"
          : "An error has occurred! Please try again!",
    });
  };

  const formItemLayout =
    formLayout === "horizontal"
      ? { labelCol: { span: 4 }, wrapperCol: { span: 14 } }
      : null;

  const buttonItemLayout =
    formLayout === "horizontal"
      ? { wrapperCol: { span: 14, offset: 4 } }
      : null;

  const onFinish = async (values: FormValueProps) => {
    console.log("Received values of form: ", values);
    try {
      await postCar(values);
      form.resetFields();
      openNotificationWithIcon("success");
    } catch (error) {
      console.error("Error while submitting: ", error);
      openNotificationWithIcon("error");
    }
  };

  return (
    <>
      {contextHolder}
      <Form
        {...formItemLayout}
        layout={formLayout}
        form={form}
        initialValues={{ layout: formLayout }}
        style={{ maxWidth: formLayout === "inline" ? "none" : 600 }}
        onFinish={onFinish}
      >
        {fields.map(field => (
          <Form.Item
            key={field.name}
            label={field.label}
            name={field.name}
            rules={[{ required: true, message: "Please input a value" }]}
          >
            <Input />
          </Form.Item>
        ))}
        <Form.Item {...buttonItemLayout}>
          <Button type="primary" htmlType="submit">
            Submit
          </Button>
        </Form.Item>
      </Form>
    </>
  );
};

export default FormComponent;

```

---

**Step 4. Optional:** I am contemplating the incorporation of a dark mode feature into our application. To achieve this, adjustments to Ant Design configurations within the App.tsx file will be necessary. For detailed guidance, I recommend referring to the official Ant Design documentation

> NOTE: To verify the successful retrieval of environmental variables from our backend, facilitated by Serverless Application Model (SAM), one can execute a console log using the following code snippet

```javascript
console.log(`${process.env.REACT_APP_ENDPOINT}inventory`);

```

If you are using VITE, please checkout [VITE's documentation](https://vitejs.dev/guide/env-and-mode.html).


**App.tsx**
```javascript
import { ConfigProvider, theme } from "antd";
import Home from "./components/Home";
import "./App.css";

function App() {
  const { darkAlgorithm } = theme;

  console.log(`${process.env.REACT_APP_ENDPOINT}inventory`);

  return (
    <>
      <ConfigProvider
        theme={{
          algorithm: darkAlgorithm,
        }}
      >
        <Home />
      </ConfigProvider>
    </>
  );
}

export default App;
```

Lets push everything to Github and lets check the final result of our project

[https://main.d56vr8yu7jeq7.amplifyapp.com/](https://main.d56vr8yu7jeq7.amplifyapp.com/)