Skip to content

Latest commit

 

History

History
1126 lines (943 loc) · 28.5 KB

File metadata and controls

1126 lines (943 loc) · 28.5 KB

Advanced REST API Topics

Complete Project, Authentication & More

Contents

  1. Planning a REST API
  2. CURD Operations & Endpoints
  3. Validation
  4. Image Upload
  5. Authentication

기존 프로젝트에서 구현한 기능과 REST API 로 구현할 때 비교

  • Node + Express App Setup => No changes
  • Routing / Endpoints => No changes, more Http methods
  • Handling Request & Responses => Parse + Send JSON Data , no Views
  • Request Validation => No changes
  • Database Communication => No changes
  • Files, Uploads, Downloads => No changes (only on client - side)
  • Session & Cookies => No Session & Cookie Usage
  • Authentication => Different Authentication Approach

1. Front API+ REST API 서버 연동하기

React.js 기본 설정

  1. 미리 제작한 프로젝트 가져오기

  2. $npm i 로 node_modules 설치하기

  3. src/pages/Feed/feed.js

    • REST API 서버로 보낼 URL 설정하기

       loadPosts = direction => {
          if (direction) {
            this.setState({ postsLoading: true, posts: [] });
          }
          let page = this.state.postPage;
          if (direction === 'next') {
            page++;
            this.setState({ postPage: page });
          }
          if (direction === 'previous') {
            page--;
            this.setState({ postPage: page });
          }
          fetch('http://localhost:8080/feed/posts')
            .then(res => {
              if (res.status !== 200) {
                throw new Error('Failed to fetch posts.');
              }
              return res.json();
            })
            .then(resData => {
              this.setState({
                posts: resData.posts,
                totalPosts: resData.totalItems,
                postsLoading: false
              });
            })
            .catch(this.catchError);
        };
    • localhost:3000에 접속하면 포스트가 생성된 것을 볼 수 있다.

New Post 생성하기

  1. react : src/pages/Feed/Feed.js

    • finishEditHandler메소드 URL 수정하기

      finishEditHandler = postData => {
          this.setState({
            editLoading: true
          });
          // Set up data (with image!)
          let url = 'http://localhost:8080/feed/post';
          let method = 'POST';
          if (this.state.editPost) {
            url = 'URL';
          }
      
          fetch(url, {
            method: method,
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              title: postData.title,
              content: postData.content
            })
          })
            .then(res => {
              if (res.status !== 200 && res.status !== 201) {
                throw new Error('Creating or editing a post failed!');
              }
              return res.json();
            })
          
          ...
  2. REST-API : controllers/feed.js

    • createPostFront Server로 보내줄 데이터 추가하기

      exports.createPost = (req, res, next) => {
        const title = req.body.title;
        const content = req.body.content;
        console.log(title);
        // Create post in db
        // status : 201 새로운 리소스 생성을 성공했다.
        res.status(201).json({
          message: 'Post created successfully!',
          post: {
            id: new Date().toISOString(),
            title: title,
            content: content,
            creator: { name: 'kooks7' },
            createdAt: new Date()
          }
        });
      };

REST API(Server-Side) validation 추가하기 - express-validator

  1. $npm i --save express-validator

  2. routes/feed.js

    • expressvalidator 가져오고 각 라우터에 검증 로직 넣기

      exports.getPosts = (req, res, next) => {
        res.status(200).json({
          posts: [
            {
              _id: '1',
              title: 'First Posts',
              content: 'This is the first post!',
              imageUrl: 'images/home4.jpg',
              creator: {
                name: 'kooks7'
              },
              createdAt: new Date()
            }
          ]
        });
      };
  3. controllers/feed.js

    • 포스트 생성할 때 오류 검증하기

      const { validationResult } = require('express-validator/check');
      
      ...
      
      exports.createPost = (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
          return res
            .status(422)
            .json({ message: '올바른 값을 입력해주세요', errors: errors.array() });
        }
        const title = req.body.title;
        const content = req.body.content;
        // Create post in db
        // status : 201 새로운 리소스 생성을 성공했다.
        res.status(201).json({
          message: 'Post created successfully!',
          post: {
            id: new Date().toISOString(),
            title: title,
            content: content,
            creator: { name: 'kooks7' },
            createdAt: new Date()
          }
        });
      };

DB 설정하기 - MongoDB

mongoose 설치하기

  1. $npm i --save mongoose

  2. app.js

    • mongoose 세팅하기

      const mongoose = require('mongoose');
      const MONGODB_URI = 'mongodb://localhost:27017/test';
      
      ...
      
      mongoose
        .connect(MONGODB_URI)
        .then(result => {
          app.listen(8080);
        })
        .catch(err => {
          console.log(err);
        });
  3. models/post.js

    • post 스키마 만들기

      const mongoose = require('mongoose');
      const Schema = mongoose.Schema;
      
      const postSchema = new Schema(
        {
          title: {
            type: String,
            required: true
          },
          imageUrl: {
            type: String,
            required: true
          },
          content: {
            type: String,
            required: true
          },
          creator: {
            type: Object,
            required: true
          }
        },
        { timestamps: true }
      );
      
      module.exports = mongoose.model('Post', postSchema);
  4. controllers/feed.js

    • createPost와 mongodb 연동하기

      exports.createPost = (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
          return res
            .status(422)
            .json({ message: '올바른 값을 입력해주세요', errors: errors.array() });
        }
        const title = req.body.title;
        const content = req.body.content;
        const post = new Post({
          title: title,
          content: content,
          // 파일 업로드 전까지 하드코딩
          imageUrl: 'images/home4.jpg',
          creator: { name: 'kooks7' }
        });
        post
          .save()
          .then(result => {
            console.log(result);
            res.status(201).json({
              message: 'Post created successfully!',
              post: result
            });
          })
          .catch(err => {
            console.log(err);
          });
      };

Image 업로드 하기 위해 static 페이지 설정하기

  1. app.js

    • static 폴더 설정하기

      const path = require('path');
      
      app.use('/images', express.static());

Node.js Error객체 이용해서 Error 다루기

  1. controllers/feed.js

    exports.createPost = (req, res, next) => {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        const error = new Error('올바른 값을 입력해주세요');
        error.statusCode = 422;
        throw error;
      }
      const title = req.body.title;
      const content = req.body.content;
      const post = new Post({
        title: title,
        content: content,
        imageUrl: 'images/home4.jpg',
        creator: { name: 'kooks7' }
      });
      post
        .save()
        .then(result => {
          console.log(result);
          res.status(201).json({
            message: 'Post created successfully!',
            post: result
          });
        })
        .catch(err => {
          if (!err.statusCode) {
            err.statusCode = 500;
          }
          next(err);
        });
    };
  2. app.js

    • 에러 핸들링 라우터 만들기

      app.use((error, req, res, next) => {
        console.log(error);
        const status = error.statusCode || 500;
        const message = error.message;
        res.status(status).json({ message: message });
      });

단일 게시물 가져오기

  1. routes/feed.js

    • getPost router 만들기

      router.get('/post/:postId', feedController.getPost);
  2. controllers/feed.js

    • getPostcontrollers 만들기

      exports.getPost = (req, res, next) => {
        const postId = req.params.postId;
        Post.findById(postId)
          .then(post => {
            if (!post) {
              const error = new Error('Could not find post.');
              error.statusCode = 404;
              throw error;
            }
            res.status(200).json({ message: 'Post fetched', post: post });
          })
          .catch(err => {
            if (!err.statusCode) {
              err.statusCode = 500;
            }
            next(err);
          });
      };
    • 기존 파일 가져오는 로직 수정하기 (getPosts)

      exports.getPosts = (req, res, next) => {
        Post.find()
          .then(posts => {
            res
              .status(200)
              .json({ message: 'Fetched posts successfully.', posts: posts });
          })
          .catch(err => {
            if (!err.statusCode) {
              err.statusCode = 500;
            }
          });
      };
  3. 프론트페이지 수정하기 src/pages/Feed/SinglePost/SinglePost.js

    • componentDidMount()

       componentDidMount() {
          const postId = this.props.match.params.postId;
          fetch('http://localhost:8080/feed/post/' + postId)
            .then(res => {
              if (res.status !== 200) {
                throw new Error('Failed to fetch status');
              }
              return res.json();
            })
            .then(resData => {
              this.setState({
                title: resData.post.title,
                author: resData.post.creator.name,
                image: 'http://localhost:8080/' + resData.post.imageUrl,
                date: new Date(resData.post.createdAt).toLocaleDateString('en-US'),
                content: resData.post.content
              });
            })
            .catch(err => {
              console.log(err);
            });
        }

Image 업로드 기능 넣기

windows 운영체제에서 서버를 실행하면 경로처리를 다르게 해줘야 한다.

  1. 파일 업로드시 발생하는 CORS 오류 해결하기 위해서 uuid 설치하기 $npm i --save uuid

  2. multer 설치하기 $npm i --save multer

  3. app.js

    • multer 로직 구성하기

      const multer = require('multer');
      const uuidv4 = require('uuid/v4');
      
      const fileStorage = multer.diskStorage({
        destination: (req, file, cb) => {
          cb(null, 'images');
        },
        filename: (req, file, cb) => {
          cb(null, uuidv4());
        }
      });
      
      const fileFilter = (req, file, cb) => {
        if (
          file.mimetype === 'image/png' ||
          file.mimetype === 'image/jpg' ||
          file.mimetype === 'image/jpeg'
        ) {
          cb(null, true);
        } else {
          cb(null, false);
        }
      };
      
      app.use(multer({storage: fileStorage, fileFilter: fileFilter}).single('image'))
  4. controllers/feed.js

    • createPost 에 multer로 파일 업로드 기능 넣기

      exports.createPost = (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
          const error = new Error('올바른 값을 입력해주세요');
          error.statusCode = 422;
          throw error;
        }
      
        if (!req.file) {
          const error = new Error('No image provided.');
          error.statusCode = 422;
          throw error;
        }
      
        const imageUrl = req.file.path.replace('\\', '/');
        const title = req.body.title;
        const content = req.body.content;
        const post = new Post({
          title: title,
          content: content,
          imageUrl: imageUrl,
          creator: { name: 'kooks7' }
        });
        post
          .save()
          .then(result => {
            console.log(result);
            res.status(201).json({
              message: 'Post created successfully!',
              post: result
            });
          })
          .catch(err => {
            if (!err.statusCode) {
              err.statusCode = 500;
            }
            next(err);
          });
      };
  5. Front page : src/pages/Feed/SinglePost/Feed.js

    • 프론트 페이지에 formData 넣기

      finishEditHandler = postData => {
          this.setState({
            editLoading: true
          });
          // 
          const formData = new FormData();
          formData.append('title', postData.title);
          formData.append('content', postData.content);
          formData.append('image', postData.image);
          let url = 'http://localhost:8080/feed/post';
          let method = 'POST';
          if (this.state.editPost) {
            url = 'URL';
          }
      
          fetch(url, {
            method: method,
            body: formData
          })
            .then(res => {
              if (res.status !== 200 && res.status !== 201) {
                throw new Error('Creating or editing a post failed!');
              }
              return res.json();
            })
            .then(resData => {
              console.log(resData);
              const post = {
                _id: resData.post._id,
                title: resData.post.title,
                content: resData.post.content,
                creator: resData.post.creator,
                createdAt: resData.post.createdAt
              };
              this.setState(prevState => {
                let updatedPosts = [...prevState.posts];
                if (prevState.editPost) {
                  const postIndex = prevState.posts.findIndex(
                    p => p._id === prevState.editPost._id
                  );
                  updatedPosts[postIndex] = post;
                } else if (prevState.posts.length < 2) {
                  updatedPosts = prevState.posts.concat(post);
                }
                return {
                  posts: updatedPosts,
                  isEditing: false,
                  editPost: null,
                  editLoading: false
                };
              });
            })
            .catch(err => {
              console.log(err);
              this.setState({
                isEditing: false,
                editPost: null,
                editLoading: false,
                error: err
              });
            });
        };

게시물 업데이트

  1. routes/feed.js

    • put 메소드 사용하기

      router.put('/post/:postId')
  2. controllers/feed.js

    exports.updatePost = (req, res, next) => {
      const postId = req.params.postId;
      const title = req.body.title;
      const content = req.body.content;
      let imageUrl = req.body.image;
      if (req.file) {
        imageUrl = req.file.path;
      }
    
      if (!imageUrl) {
        const error = new Error('No file picked.');
        error.statusCode = 422;
        throw error;
      }
    };
  3. Front page : src/pages/Feed/SinglePost/Feed.js

    • 프론트페이지 로직 구성하기

          fetch('http://localhost:8080/feed/posts')
            .then(res => {
              if (res.status !== 200) {
                throw new Error('Failed to fetch posts.');
              }
              return res.json();
            })
            .then(resData => {
              this.setState({
                posts: resData.posts.map(post => {
                  return {
                    ...post,
                    imagePath: post.imageUrl
                  };
                }),
                totalPosts: resData.totalItems,
                postsLoading: false
              });
            })
            .catch(this.catchError);
        };
  4. routes/feed.js

    • validator 설정하고 updatePost 가져오기

      router.put(
        '/post/:postId',
        [
          body('title')
            .trim()
            .isLength({ min: 5 }),
          body('content')
            .trim()
            .isLength({ min: 5 })
        ],
        feedController.updatePost
      );
  5. controllers/feed.js

    • update controller 작성하기

      exports.updatePost = (req, res, next) => {
        const postId = req.params.postId;
        const errors = validationResult(req);
      
        if (!errors.isEmpty()) {
          const error = new Error('올바른 값을 입력해주세요');
          error.statusCode = 422;
          throw error;
        }
      
        const title = req.body.title;
        const content = req.body.content;
        let imageUrl = req.body.image;
        if (req.file) {
          imageUrl = req.file.path;
        }
      
        if (!imageUrl) {
          const error = new Error('No file picked.');
          error.statusCode = 422;
          throw error;
        }
        Post.find(postId)
          .then(post => {
            if (!post) {
              const error = new Error('Could not find post.');
              error.statusCode = 404;
              throw err;
            }
      
            // 기존 이미지 지우기
            if (imageUrl !== post.imageUrl) {
              clearImage(post.imageUrl);
            }
      
            post.title = title;
            post.imageUrl = imageUrl;
            post.content = content;
            return post.save();
          })
          .then(result => {
            res.status(200).json({ message: 'Post updated!', post: result });
          })
          .catch(err => {
            if (!err.statusCode) {
              err.statusCode = 500;
            }
            next(err);
          });
      };
      
      // 파일 path 받아서 해당 파일 삭제하는 Helper Function
      const clearImage = filePath => {
        filePath = path.join(__dirname, '..', filePath);
        fs.unlink(filePath, err => console.log(err));
      };
  6. Front : src/pages/Feed/Feed.js

    • finishEditHandler 작성하기

        finishEditHandler = postData => {
          this.setState({
            editLoading: true
          });
          const formData = new FormData();
          formData.append('title', postData.title);
          formData.append('content', postData.content);
          formData.append('image', postData.image);
          let url = 'http://localhost:8080/feed/post';
          let method = 'POST';
          if (this.state.editPost) {
            url = 'http://localhost:8080/feed/post/' + this.state.editPost._id;
            method = 'PUT';
          }
  7. CORS 해결하기

    • cors 모듈 사용하기 기존 방법으로 사용해도 put 메소드로 요청을 보낼 때 cors 오류가 떠서 cors 모듈을 사용

      1. $ npm i --save cors

      2. app.js 모든 라우터에 cors 적용하기

        const cors = require('cors');
        
        app.use(cors());

post 삭제하기

  1. controllers/feed.js

    • controller 작성하기 로직은 post가 있는지 찾는다. => 있으면 저장된 사진을 삭제한다. => 해당 포스터를 DB에서 삭제한다.

      exports.deletePost = (req, res, next) => {
        const postId = req.params.postId;
        Post.findById(postId)
          .then(post => {
            if (!post) {
              const error = new Error('Could not find post.');
              error.statusCode = 404;
              throw err;
            }
            // Check logged in user
            clearImage(post.imageUrl);
            return Post.findByIdAndRemove(postId);
          })
          .then(result => {
            console.log(result);
            res.status(200).json({ message: 'Delete Post.' });
          })
          .catch(err => {
            if (!err.statusCode) {
              err.statusCode = 500;
            }
            next(err);
          });
      };
  2. routes/feed.js 라우터 만들기

    router.delete('/post/:postId', feedController.deletePost);
  3. Front: ...../Feed.js 프론트 페이지 수정

      deletePostHandler = postId => {
        this.setState({ postsLoading: true });
        fetch('http://localhost:8080/feed/post/' + postId, {
          method: 'DELETE'
        })
          .then(res => {
            if (res.status !== 200 && res.status !== 201) {
              throw new Error('Deleting a post failed!');
            }
            return res.json();
          })
          .then(resData => {
            console.log(resData);
            this.setState(prevState => {
              const updatedPosts = prevState.posts.filter(p => p._id !== postId);
              return { posts: updatedPosts, postsLoading: false };
            });
          })
          .catch(err => {
            console.log(err);
            this.setState({ postsLoading: false });
          });
      };

Pagination

  1. React Front Page : .../Feed.js

    • 쿼리 파라미터로 페이지 나타내기

      loadPosts = direction => {
          if (direction) {
            this.setState({ postsLoading: true, posts: [] });
          }
          let page = this.state.postPage;
          if (direction === 'next') {
            page++;
            this.setState({ postPage: page });
          }
          if (direction === 'previous') {
            page--;
            this.setState({ postPage: page });
          }
          fetch('http://localhost:8080/feed/posts?page=' + page)
            .then(res => {
              if (res.status !== 200) {
                throw new Error('Failed to fetch posts.');
              }
              return res.json();
            })
            .then(resData => {
              this.setState({
                posts: resData.posts.map(post => {
                  return {
                    ...post,
                    imagePath: post.imageUrl
                  };
                }),
                totalPosts: resData.totalItems,
                postsLoading: false
              });
            })
            .catch(this.catchError);
        };
  2. controllers/feed.js

    • getPosts 22강에서 했던 pagination 과 동일한 로직으로 페이지 구성하기

      exports.getPosts = (req, res, next) => {
        const currentPage = req.query.page || 1;
        const perPage = 2;
        let totalItems;
        // 총 item 갯수 세기
        Post.find()
          .countDocuments()
          .then(count => {
            totalItems = count;
            return Post.find()
              .skip((currentPage - 1) * perPage)
              .limit(perPage);
          })
          .then(posts => {
            res.status(200).json({
              message: 'Fetched posts successfully.',
              posts: posts,
              totalItems: totalItems
            });
          })
          .catch(err => {
            if (!err.statusCode) {
              err.statusCode = 500;
            }
          });
      };

사용자 추가하기

  1. models/user.js

    • user 모델 구성하기

      const mongoose = require('mongoose');
      const Schema = mongoose.Schema;
      
      const userSchecma = new Schema({
        email: {
          type: String,
          required: true
        },
        password: {
          type: String,
          required: true
        },
        name: {
          type: String,
          required: true
        },
        status: {
          type: String,
          default: 'I am new!'
        },
        post: [
          {
            type: Schema.Types.ObjectId,
            ref: 'Post'
          }
        ]
      });
      
      module.exports = mongoose.model('User', userSchecma);
  2. routes/auth.js

    • 인증을 처리할 라우터 생성하기 express-validator 적용하기

      const express = require('express');
      const { body } = require('express-validator/check');
      
      const User = require('../models/user');
      const authController = require('../controllers/auth');
      
      const router = express.Router();
      
      router.put(
        '/signup',
        [
          body('email')
            .isEmail()
            .withMessage('올바른 이메일을 입력해주세요.')
            // E-mail 이미 존재하는지 체크 하는 로직
            .custom(value => {
              return User.findOne({ email: value }).then(userDoc => {
                if (userDoc) {
                  return Promise.reject('이메일이 이미 존재합니다.');
                }
              });
            })
            .normalizeEmail(),
          body('password')
            .trim()
            .isLength({ min: 5 }),
          body('name')
            .trim()
            .not()
            .isEmpty()
        ],
        authController.signup
      );
      
      module.exports = router;
    • app.js에 설정

      const authRoutes = require('./routes/auth');
      
      app.use('/auth', authRoutes);
  3. controllers/auth.js

    • signup router 만들기

      const { validationResult } = requrie('express-validator/check');
      
      const User = require('../models/user');
      
      exports.signup = (req, res, next) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
          const error = new Error('Validation failed.');
          error.statusCode = 422;
          error.data = errors.array();
          throw error;
        }
        const email = req.body.email;
        const name = req.body.namel;
        const password = req.body.password;
      
        //... encrypt으로 password 로직 짜기
      };
  4. password 암호화 해서 저장하기

    • $npm i --save bcryptjs

    • controllers/auth.js에 bcrypt 가져오기

      const { validationResult } = require('express-validator/check');
      const bcrypt = require('bcryptjs');
      
      const User = require('../models/user');
      
      exports.signup = (req, res, next) => {
        const errors = validationResult(req);
      ...
        const password = req.body.password;
        bcrypt
          .hash(password, 12)
          .then(hashedPw => {
            const user = new User({
              email: email,
              password: hashedPw,
              name: name
            });
            return user.save();
          })
          .then(result => {
            res
              .status(201)
              .json({ message: '유저가 생성되었습니다.', userId: result._id });
          })
          .catch(err => {
            if (!err.statusCode) {
              err.statusCode = 500;
            }
            next(err);
          });
      };
  5. React Front Page : app.js

    • signupHandler에서 보내줄 데이터 사용자한테 받고 서버로 요청 보내기

        signupHandler = (event, authData) => {
          event.preventDefault();
          this.setState({ authLoading: true });
          fetch('http://localhost:8080/auth/signup', {
            method: 'PUT',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              email: authData.signupForm.email.value,
              password: authData.signupForm.password.value,
              name: authData.signupForm.name.value
            })
          })
            .then(res => {
              if (res.status === 422) {
                throw new Error(
                  "Validation failed. Make sure the email address isn't used yet!"
                );
              }
              if (res.status !== 200 && res.status !== 201) {
                console.log('Error!');
                throw new Error('Creating a user failed!');
              }
              return res.json();
            })
            .then(resData => {
              console.log(resData);
              this.setState({ isAuth: false, authLoading: false });
              this.props.history.replace('/');
            })
            .catch(err => {
              console.log(err);
              this.setState({
                isAuth: false,
                authLoading: false,
                error: err
              });
            });
        };