DEV Community

Cover image for REST API using AWS SAM+ AMPLIFY - Part 5
Lloyd Marcelino
Lloyd Marcelino

Posted on • Updated on

REST API using AWS SAM+ AMPLIFY - Part 5

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

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);
    });
};

Enter fullscreen mode Exit fullscreen mode

Understanding "apiEndPoints.ts":

import axios from "axios";
import { Item } from "../components/Home";
Enter fullscreen mode Exit fullscreen mode
  • 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.


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

Enter fullscreen mode Exit fullscreen mode
  • 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'.


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

Enter fullscreen mode Exit fullscreen mode
  • 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

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;

Enter fullscreen mode Exit fullscreen mode

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:


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";

Enter fullscreen mode Exit fullscreen mode
  • 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.


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;
}
Enter fullscreen mode Exit fullscreen mode

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.


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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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).


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);
      });
  }, []);
Enter fullscreen mode Exit fullscreen mode

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.



  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);
  };
Enter fullscreen mode Exit fullscreen mode

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.

 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),
      }),
    };
  });
Enter fullscreen mode Exit fullscreen mode

columns (see Ant Design Table documentations): 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.


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 />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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


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;

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

If you are using VITE, please checkout VITE's documentation.

App.tsx

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;
Enter fullscreen mode Exit fullscreen mode

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

https://main.d56vr8yu7jeq7.amplifyapp.com/

Top comments (0)