Upload form data with file using Multer & MYSQL in NextJs

The previous tutorial taught you how to create API endpoints to filter MYSQL database, pagination, and protect the API using middleware. This tutorial teaches you how to upload user form data with image file using Multer and delete data from MYSQL database.
From the project folder, execute the following command to install Multer:

npm install multer

Then open api/user/[slug].js file to add post, put, and delete routes. To use multer to parse request body and upload files, you need to disable default body parser by setting bodyParser to false. We use diskStorage() function from multer to configure local storage and file name. Multer needs file field and max count of the file to be uploaded. In this project, we use array() function to specify file field (photo) and the max count of the file to be uploaded (1).  We can access text parts of a submitted form data by extracting body from the request. To access file parts, you need to extract files from the request.
.....
import bcrypt from 'bcryptjs';
import multer from 'multer';
import path from 'path';

export const config = {
  api: {
    bodyParser: false, // disable default body parse
  },
};
// storage configuration
var storage = multer.diskStorage({
   destination: function (req, file, callback) {
     callback(null, './public');
   },
   filename: function (req, file, callback) {
       callback(null, file.fieldname + '-' +
Date.now()+ path.extname(file.originalname));
     
   }
 });
/*
- return Multer instance to process multipart/form-data
-specify file field and max count of file to be uploaded
*/
var upload = multer({ storage : storage }).array('photo',1);
   

const handler = nextConnect()
.use(middleware)
.post(async (req, res) => { // handle post route

   upload(req,res,async function(err) {
      if(err) {
          return res.end("Error uploading file."+err);
      }


      const {body} = req; // extract text parts from submitted form data
      const {username,email,password}=body;
      let password_encrypted= await bcrypt.hash(password, 10);
      const f = req.files && req.files[0]?{photo:req.files[0].filename}:null;
   
 
      const newUser = db.User.create(
         {
         username:username,
         email:email,
         password: password_encrypted,      
         ...f,
        },
        );
        return res.status(200).json({
         status: 'success',
         message: 'done',
         data: newUser,
        });
   
   })

.put(async (req, res) => { // handle put route

   upload(req,res,async function(err) {
      if(err) {
          return res.end("Error uploading file."+err);
      }

      const {body} = req;
      const {username,email, password}=body;

      const p = password && password!=='undefined'?
{ password:await bcrypt.hash(password, 10)}:null;
      const f = req.files && req.files[0]?{photo:req.files[0].filename}:null;
      const updateFields = {username:username,email:email,...p,...f};
     
      const updatedRows = db.User.update(
         updateFields,
        {
         where: { id: body.id },
         }
        );
        return res.status(200).json({
         status: 'success',
         message: 'done',
         data: updatedRows,
        });
   
   })

.delete(async (req, res) => { // handle delete route
 
   const { slug } = req.query;
   const userId = slug;
 
   await db.User.destroy({
       where: { id: userId },
    });
    res.status(200).send({
      message:
         "Deleted"
    });
})
............

Now our API endpoints are ready. Let modify page/user/list.js file to access the APIs. The url /api/user/upload and /api/user/${id} are handled by /api/user/[slug] route. If the form is in edit mode, the /api/user/upload is handled by put method of the API endpoint. Otherwise, it is handled by post method. The /api/user/${id} matches to the delete method.

import React,{ useState, useEffect } from "react";
import axios from "axios";
import { FaTrash } from "react-icons/fa"
import { FaEdit } from "react-icons/fa"
import { BsFilterCircle,BsFileEarmarkExcel,BsPlus } from "react-icons/bs"
import * as FileSaver from "file-saver";
import * as XLSX from "xlsx";


import {
    Label,Form,FormGroup,Input,
    Button,Col,Table,
    Modal, ModalHeader, ModalBody, ModalFooter,
    Popover, PopoverHeader, PopoverBody,
  } from "reactstrap";
  
import {getAppCookies } from "../../middleware/utils";

import Layout from "../../components/layout";
import Router from 'next/router';

// global variables
var start=0;
var step=4;

function UserList(props) {
    //vairables/
    const {token } = props;
    const [users,setUsers]=useState(null);
    const [modal,setModal]=useState(false);
    const [popover,setsetPopover]=useState(false);
    const [inputs, setInputs] = useState({});
    const [id,setId]=useState(0);
    const [refresh,setRefresh]=useState(false);
    const [sorted,setSorted]=useState(false);
    const [isEditMode,setIsEditmode]=useState(false);
    const [numRows,setNumRows]=useState(0);
    const hstyle={textDecoration: 'none'};
    const [file,setFile]=useState(null);

    
  
    const pstyle={
      marginRight: '20px',
      textDecoration: 'none',
     };
   
    // fetch data from  server using Axios
    useEffect(() => {
     if(token){
      axios
      .get(`/api/user/list?name=${inputs.name}&email=${inputs.email_addr}&fromid=${inputs.fromid}&toid=${inputs.toid}&start=${start}&limit=${step}`,{
        headers:{
            "Content-type":"application/json",
            "authorization": `Bearer ${token}`,
        },

       
      })
      .then((res) => {
        parseResult(res.data);
      
      }) 
      .catch((err) => console.log(err));
        
       }
       else{
            Router.push('/user/login'); // redirect to login page
       }
     }, [refresh]);


     async function handleSubmit(e) {
      e.preventDefault();
  
      let form_data = new FormData();
      form_data.append('id', id);
      form_data.append('username', inputs.username);
      form_data.append('email', inputs.email);
      form_data.append('password', inputs.password);
      if(file!=null){
          
          form_data.append('photo', file,file.name);
          
      }
    
      for (var key of form_data.entries()) { 
        console.log(key[0] + ', ' + key[1]);
      }
      
      axios({
        method: isEditMode?'put':'post',
        url: `/api/user/upload`,
        data: form_data,
        headers: { 
        "authorization": `Bearer ${token}`
        }
      }).then(response=>{
       
        let resobj=JSON.parse(JSON.stringify(response.data));
        if(resobj.status==='success'){
          setRefresh(!refresh); 
          setFile(null);
          toggle();
        }
      });


    };


    const searchUser= () =>{

            start=0; // reset start
            axios({
              method: 'get',
              url: `/api/user/list?name=${inputs.name}&email=${inputs.email_addr}&fromid=${inputs.fromid}&toid=${inputs.toid}&start=${start}&limit=${step}`,
              headers: { "Content-type":"application/json","authorization": `Bearer ${token}`}
            }).then(res=>{
              parseResult(res.data);
              
            });
            
        
      };
  const deleteUser = (id) => {
      if(confirm("Are you sure to delete?")) {
        axios({
          method: 'delete',
          url: `/api/user/${id}/`,
          headers: { "Content-type":"application/json","authorization": `Bearer ${token}`}
        }).then(response=>{
          console.log("deleted",response.data);
          setRefresh(!refresh); 
          
        });
       }
      };  
  const editUser = (item) => {
      
        setIsEditmode(true);
        setId(item.id);
        setInputs(values => ({...values, ['username']: item.username}));
        setInputs(values => ({...values, ['email']: item.email}));
       
        toggle(!modal);
      };     
    
  function toggle() {
    if(!modal) setFile(null);
    setModal(!modal);
    

  }
  function togglePop() {
    setsetPopover(!popover);
    

  }
  function openAddNewModel() {
    setIsEditmode(false);
    toggle();

  }
  const handleImageChange = (e) => {
    setFile(e.target.files[0]);
  };
  const handleChange = (event) =>{
    const name = event.target.name;
    const value = event.target.value;
    setInputs(values => ({...values, [name]: value}))
  };

  const handlePaging = (mv) =>{
      if(mv==='next') {
        if(start<numRows-step) start=start+step;
      }
      else if(mv==='prev') {
        if(start>=step) start=start-step;
      
      }
       
       axios({
        method: 'get',
        url: `/api/user/list?name=${inputs.name}&email=${inputs.email_addr}&fromid=${inputs.fromid}&toid=${inputs.toid}&start=${start}&limit=${step}`,
        headers: { "Content-type":"application/json","authorization": `Bearer ${token}`}
      }).then(res=>{
        parseResult(res.data);
        
      });

  }
  const handleHeaderClick = (field) =>{
    // sort records
    if(field!=='id') users.sort((a,b) => a[field].localeCompare(b[field]));
    else users.sort((a,b) => b[field]-a[field]);
    // refresh UI
    setSorted(!sorted);
    
      
  }
  const parseResult = (data) =>{
      let dt=JSON.parse(JSON.stringify(data));
      if(dt.message==='success'){
        setUsers(dt.data);
        setNumRows(dt.numrow);
      }
  }

   // export to excel
   const fileType =
   "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8";
 const fileExtension = ".xlsx";

 const exportToCSV = (apiData, fileName) => {
   const ws = XLSX.utils.json_to_sheet(apiData);
   const wb = { Sheets: { data: ws }, SheetNames: ["data"] };
   const excelBuffer = XLSX.write(wb, { bookType: "xlsx", type: "array" });
   const data = new Blob([excelBuffer], { type: fileType });
   FileSaver.saveAs(data, fileName + fileExtension);
 };
   //

    return (
     
      <Layout
        token={token}
        title='User Admin Panel'
      >
  
    
    <div className="container">
      <div style={{float: 'right'}}>
        <Button color="primary" id="btfilter"><BsFilterCircle/></Button>
      </div>
      <Popover placement="top" isOpen={popover} target="btfilter" toggle={togglePop}>
          <PopoverHeader>Filter</PopoverHeader>
          <PopoverBody>
          <FormGroup row>
         
          <Col sm={10}>
            <Input type="text" value={inputs.name || ""} name="name" id="name" placeholder="By User name" bsSize="lg" onChange={handleChange}/>
          </Col>
        </FormGroup>
        <FormGroup row>
         
          <Col sm={10}>
            <Input type="text" value={inputs.email_addr || ""} name="email_addr" id="email_addr" placeholder="By Email"  onChange={(e) =>handleChange(e)} />
          </Col>
        </FormGroup>

        <FormGroup row>
        <Col sm={6}>
           <Input type="number"  value={inputs.fromid || ""}  name="fromid" id="fromid" placeholder="From ID"  onChange={(e) =>handleChange(e)} />
         </Col>
         <Col sm={6}>
           <Input type="number"  value={inputs.toid || ""}  name="toid" id="toid" placeholder="To ID"  onChange={(e) =>handleChange(e)} />
         </Col>
       </FormGroup>

        <Button color="primary" onClick={(e) =>searchUser()}>Ok</Button>
          </PopoverBody>
      </Popover>
      
      <div style={{float: 'right',marginRight: '10px'}}>
        <Button color="primary"  onClick={(e) => exportToCSV(users, 'users')}><BsFileEarmarkExcel/></Button>
      </div>
      <div>Users List ({numRows} items)</div> 
      
      {
        
      <Table striped bordered hover>
      <thead> 
      <tr>
        <th>
        <a style={hstyle} href="#" onClick={() => handleHeaderClick("id")}>ID</a></th>
        <th >
        <a style={hstyle} href="#" onClick={() => handleHeaderClick("username")}>User name</a></th>
        <th><a style={hstyle} href="#" onClick={() => handleHeaderClick("email")}>Email</a></th>
        <th colSpan={3}></th>
        </tr>
      </thead>
      <tbody>
      {
       users && users.map((item, i) => {
        return (
            <tr key={item.id}>
            <td >{item.id}</td>
            <td >{item.username}</td>
            <td >{item.email}</td>
            <td><img style={{width: '50px',height:'50px'}} src={"/"+item.photo}/></td>
            <td > <Button color="primary"  onClick={() =>editUser(item)}>
            <FaEdit style={{ color: "white", fontSize: "12px" }} />

            </Button></td>
            <td > <Button color="primary" onClick={() =>deleteUser(item.id)}>
            <FaTrash style={{ color: "red", fontSize: "12px" }} />

            </Button></td>
            </tr>
        );	

         })
      }
      </tbody>
    </Table> 
    }
    {

      (start-step>=0 && numRows>step) && <a href="#" onClick={() =>handlePaging('prev')} style={pstyle} to={"#"}>{"<"}</a>
      
      }  
    {

     (numRows-start>=step  && numRows>step) && <a href="#"  onClick={() =>handlePaging('next')}   style={pstyle} to={"#"}>{">"}</a>  
    }
    <div style={{float: 'right'}}>
      <Button color="primary"  onClick={openAddNewModel}><BsPlus/></Button>
    </div>
    <Modal isOpen={modal} toggle={toggle}>
          <ModalHeader toggle={toggle}>{isEditMode?'Edit User':'Add User'}</ModalHeader>
          <ModalBody>
            
        <Form>
        <FormGroup row>
          <Label for="username" sm={3} size="lg">Username</Label>
          <Col sm={8}>
            <Input type="text" value={inputs.username || ""} name="username" id="username" placeholder="User name" bsSize="lg" onChange={(e) =>handleChange(e)} required />
          </Col>
        </FormGroup>
        <FormGroup row>
          <Label for="email" sm={3}>Email</Label>
          <Col sm={8}>
            <Input type="text" value={inputs.email || ""} name="email" id="email" placeholder="Email"  onChange={(e) =>handleChange(e)} required />
          </Col>
        </FormGroup>

        <FormGroup row>
          <Label for="password" sm={3}>Password</Label>
          <Col sm={8}>
            <Input type="password" value={inputs.password || ""} name="password" id="password" placeholder="Password"  onChange={(e) =>handleChange(e)} required />
          </Col>
        </FormGroup>

        <FormGroup>
            <p><Label for="file">File</Label>
              <Input 
                type="file"
                id="file"
                name="file"
				        accept="image/png, image/jpeg"  onChange={handleImageChange} 
              
              />
              
              </p>
             
            </FormGroup>
      </Form>

          </ModalBody>
          <ModalFooter>
            <Button color="primary" onClick={handleSubmit}>Ok</Button>
            <Button color="secondary" onClick={toggle}>Cancel</Button>
          </ModalFooter>
        </Modal>
      
    </div>
    </Layout>
  );
}
/* getServerSideProps */
export async function getServerSideProps(context) {
  const { req } = context;

  const token = getAppCookies(req).token || '';
  return {
    props: {

      token,
    },
  };
}

export default UserList;

Save the project. Make sure MYSQL is running. You are able to add new user with and image file, updating existing users, and delete users from the database.




Demo Video

Comments

Popular posts from this blog

Protect APIs & Data filtering & paging in NextJs & MYSQL

Migration using Sequelize with MYSQL database in NextJs