Dicoding Submission Bookshelf API – Belajar Membuat Aplikasi Back-End untuk Pemula

dicoding submission bookshelf api belajar membuat aplikasi backend untuk pemula

Berikut adalah tutorial membuat Project Submission Bookshelf API pada Kelas Belajar Membuat Aplikasi Back-End untuk Pemula.

1. Pastikan sudah menginstall nodejs pada PC. Kemudian yang perlu dilakukan adalah membuat folder untuk project.

2. Buka command prompt pada folder tersebut, kemudian ketikkan perintah berikut untuk membuat proyek node.js.

npm init --y

3. Buat folder baru dalam folder proyek tersebut dengan nama src. Kemudian dalam folder tersebut buat file bernama server.js, routes.js, handler.js, dan items.js. Untuk percobaan, tambahkan kode berikut pada file server.js.

console.log("Berhasil compile");

4. Untuk mempermudah dalam pengembangan, perlu menggunakan tools nodemon. Dengan nodemon, kita tidak perlu menjalankan ulang server ketika terjadi perubahan pada kode JavaScript karena nodemon yang mendeteksi perubahan kode JavaScript dan mengeksekusi ulang secara otomatis. Untuk dapat menggunakannya, jalankan perintah berikut.

npm install nodemon --save-dev

5. Pada package.json, pada bagian scripts tambahkan kode berikut. start-dev digunakan untuk menjalankan project pada development stage.

// package.json
"scripts": {
    "start": "node ./src/server.js",
    "start-dev": "nodemon ./src/server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
}

6. Coba Jalankan npm run start-dev untuk menjalankan project pada development stage. Jika berhasil, maka akan menampilkan tulisan “Berhasil compile”.

7. ESLint digunakan untuk membantu menuliskan kode JavaScript dengan gaya yang konsisten. Jalankan perintah di bawah untuk menginstall ESLint dan menyimpannya pada devDependencies di package.json.

npm install eslint --save-dev

8. Jalankan perintah berikut untuk melakukan konfigurasi ESLint.

npx eslint --init

Kemudian jawab pertanyaan yang ada dengan jawaban berikut:

  • How would you like to use ESLint? -> To check, find problems, and enforce code style.
  • What type of modules does your project use? -> CommonJS (require/exports).
  • Which framework did you use? -> None of these. 
  • Does your project use TypeScript? -> N.
  • Where does your code run? -> Node (pilih menggunakan spasi).
  • How would you like to define a style for your project? -> Use a popular style guide.
  • Which style guide do you want to follow? -> (Kamu bebas memilih, sebagai contoh pilih AirBnB).
  • What format do you want your config file to be in? -> JSON.
  • Would you like to …… (seluruh pertanyaan selanjutnya) -> Y.

9. Pada package.json di bagian scripts, tambahkan script berikut.

// package.json
"scripts": {
    "start": "node ./src/server.js",
    "start-dev": "nodemon ./src/server.js",
    "lint": "eslint ./src",
    "test": "echo \"Error: no test specified\" && exit 1"
}

10. Struktur project akhir seharusnya akan seperti ini

11. Install extension ESLint di Visual Studio Code untuk mempermudah integrasi dengan text editor.

12. Selanjutnya, install HTTP server menggunakan Hapi.

npm install @hapi/hapi

13. Pada server.js, tambahkan code berikut

// server.js
const Hapi = require('@hapi/hapi');

const init = async () => {
  const server = Hapi.server({
    port: 9000,
    host: 'localhost',
  });

  await server.start();

  console.log(`Server berjalan pada ${server.info.uri}`);
};

init();

14. Pada routes.js, tambahkan kode berikut

// routes.js
const routes = [
  {
    method: 'POST',
    path: '/books',
    handler: () => {}, // Akan diubah dengan handler nanti
  },
];

module.exports = routes;

15. Hubungkan routes.js ke server.js.

// server.js
const Hapi = require('@hapi/hapi');
const routes = require('./routes');

const init = async () => {
  const server = Hapi.server({
    port: 9000,
    host: 'localhost',
  });

  server.route(routes);
  await server.start();

  console.log(`Server berjalan pada ${server.info.uri}`);
};

init();

16. Untuk mengatasi Same-Origin Policy, tambahkan kode berikut di server.js berikut.

// server.js
const Hapi = require('@hapi/hapi');
const routes = require('./routes');

const init = async () => {
  const server = Hapi.server({
    port: 9000,
    host: 'localhost',
    routes: {
      cors: {
        origin: ['*'],
      },
    },
  });

  server.route(routes);
  await server.start();

  console.log(`Server berjalan pada ${server.info.uri}`);
};

init();

17. Pada items.js, tambahkan kode berikut

// items.js
const items = [];

module.exports = items;

18. Pada handler.js, tambahkan kode berikut. Kode request.payload digunakan untuk mengubah payload JSON menjadi objek JavaScript.

// handler.js
const items = require('./items');

const addItemHandler = (request, h) => {
  const {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  } = request.payload;

  const newItem = {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  };

  items.push(newItem);
};

module.exports = { addItemHandler };

19. Tambahkan addItemHandler pada routes.js.

// routes.js
const { addItemHandler } = require('./handler');

const routes = [
  {
    method: 'POST',
    path: '/books',
    handler: addItemHandler,
  },
];

module.exports = routes;

20. Selanjutnya menginstal nanoid untuk id buku menggunakan perintah di bawah. Menggunakan nanoid versi 3.x.x karena jika menggunakan versi terbaru, nanoid tidak dapat digunakan dengan format module CommonJS.

npm install [email protected]. //x.x diganti dengan versi nanoid yang tersedia

21. Tambahkan respon ketika tidak terdapat properti name pada request body dan nilai properti readPage lebih besar dari pageCount.

// handler.js
const items = require('./items');

const addItemHandler = (request, h) => {
  const {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  } = request.payload;

  if (!name) {
    const response = h
      .response({
        status: 'fail',
        message: 'Gagal menambahkan buku. Mohon isi nama buku',
      })
      .code(400);
    return response;
  }

  if (readPage > pageCount) {
    const response = h
      .response({
        status: 'fail',
        message:
          'Gagal menambahkan buku. readPage tidak boleh lebih besar dari pageCount',
      })
      .code(400);
    return response;
  }

  const newItem = {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  };

  items.push(newItem);
};

module.exports = { addItemHandler };

22. Lengkapi handler.js dengan menambahkan beberapa atribut tambahan berikut.

// handler.js
const { nanoid } = require('nanoid');
const items = require('./items');

const addItemHandler = (request, h) => {
  const {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  } = request.payload;

  if (!name) {
    const response = h
      .response({
        status: 'fail',
        message: 'Gagal menambahkan buku. Mohon isi nama buku',
      })
      .code(400);
    return response;
  }

  if (readPage > pageCount) {
    const response = h
      .response({
        status: 'fail',
        message:
          'Gagal menambahkan buku. readPage tidak boleh lebih besar dari pageCount',
      })
      .code(400);
    return response;
  }

  const id = nanoid(16);
  const finished = pageCount === readPage;
  const insertedAt = new Date().toISOString();
  const updatedAt = insertedAt;

  const newItem = {
    id,
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    finished,
    reading,
    insertedAt,
    updatedAt,
  };

  items.push(newItem);
};

module.exports = { addItemHandler };

23. Tambahkan variabel isSuccess untuk mengecek apakah newItem sudah masuk ke array items menggunakan method filter(). Selain itu, tambahkan juga respon ketika newItem berhasil dan gagal masuk ke array items.

// handler.js
const { nanoid } = require('nanoid');
const items = require('./items');

const addItemHandler = (request, h) => {
  const {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  } = request.payload;

  if (!name) {
    const response = h
      .response({
        status: 'fail',
        message: 'Gagal menambahkan buku. Mohon isi nama buku',
      })
      .code(400);
    return response;
  }

  if (readPage > pageCount) {
    const response = h
      .response({
        status: 'fail',
        message:
          'Gagal menambahkan buku. readPage tidak boleh lebih besar dari pageCount',
      })
      .code(400);
    return response;
  }

  const id = nanoid(16);
  const finished = pageCount === readPage;
  const insertedAt = new Date().toISOString();
  const updatedAt = insertedAt;

  const newItem = {
    id,
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    finished,
    reading,
    insertedAt,
    updatedAt,
  };

  items.push(newItem);

  const isSuccess = items.filter((book) => book.id === id).length > 0;

  if (isSuccess) {
    const response = h
      .response({
        status: 'success',
        message: 'Buku berhasil ditambahkan',
        data: {
          bookId: id,
        },
      })
      .code(201);
    return response;
  }

  const response = h
    .response({
      status: 'fail',
      message: 'Buku gagal ditambahkan',
    })
    .code(500);
  return response;
};

module.exports = { addItemHandler };

24. Untuk mengetes API handler tersebut, jalankan perintah berikut.

curl -X POST -H "Content-Type: application/json" http://localhost:9000/books -d "{\"name\": \"Dimas\"}"

25. Pada handler.js, tambahkan beberapa handler lainnya sesuai requirement

// handler.js
const { nanoid } = require('nanoid');
const items = require('./items');

const addItemHandler = (request, h) => {
  const {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  } = request.payload;

  if (!name) {
    const response = h
      .response({
        status: 'fail',
        message: 'Gagal menambahkan buku. Mohon isi nama buku',
      })
      .code(400);
    return response;
  }

  if (readPage > pageCount) {
    const response = h
      .response({
        status: 'fail',
        message:
          'Gagal menambahkan buku. readPage tidak boleh lebih besar dari pageCount',
      })
      .code(400);
    return response;
  }

  const id = nanoid(16);
  const finished = pageCount === readPage;
  const insertedAt = new Date().toISOString();
  const updatedAt = insertedAt;

  const newItem = {
    id,
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    finished,
    reading,
    insertedAt,
    updatedAt,
  };

  items.push(newItem);

  const isSuccess = items.filter((book) => book.id === id).length > 0;

  if (isSuccess) {
    const response = h
      .response({
        status: 'success',
        message: 'Buku berhasil ditambahkan',
        data: {
          bookId: id,
        },
      })
      .code(201);
    return response;
  }

  const response = h
    .response({
      status: 'fail',
      message: 'Buku gagal ditambahkan',
    })
    .code(500);
  return response;
};

const getAllItemsHandler = (request, h) => {}

const getItemByIdHandler = (request, h) => {}

const editItemByIdHandler = (request, h) => {}

const deleteItemByIdHandler = (request, h) => {}

module.exports = {
  addItemHandler,
  getAllItemsHandler,
  getItemByIdHandler,
  editItemByIdHandler,
  deleteItemByIdHandler,
};

26. Tambahkan handler yang telah dibuat pada routes.js

// routes.js
const {
  addItemHandler,
  getAllItemsHandler,
  getItemByIdHandler,
  editItemByIdHandler,
  deleteItemByIdHandler,
} = require('./handler');

const routes = [
  {
    method: 'POST',
    path: '/books',
    handler: addItemHandler,
  },
  {
    method: 'GET',
    path: '/books',
    handler: getAllItemsHandler,
  },
  {
    method: 'GET',
    path: '/books/{bookId}',
    handler: getItemByIdHandler,
  },
  {
    method: 'PUT',
    path: '/books/{bookId}',
    handler: editItemByIdHandler,
  },
  {
    method: 'DELETE',
    path: '/books/{bookId}',
    handler: deleteItemByIdHandler,
  },
  {
    method: '*',
    path: '/{any*}',
    handler: () => 'Halaman tidak ditemukan',
  },
];

module.exports = routes;

27. Lengkapi getAllItemsHandler. Pada RegExp terdapat parameter ‘gi’, parameter ini berarti ‘global (mengembalikan semua yang sesuai, tidak hanya yang sesuai pertama saja)’ dan ‘insensitive’ (mengabaikan case sensitive).

const getAllItemsHandler = (request, h) => {
  const { name, reading, finished } = request.query;

  // Terdapat query name
  if (name) {
    const filteredBooksName = items.filter((book) => {
      const nameRegex = new RegExp(name, 'gi');
      return nameRegex.test(book.name);
    });

    const response = h
      .response({
        status: 'success',
        data: {
          books: filteredBooksName.map((book) => ({
            id: book.id,
            name: book.name,
            publisher: book.publisher,
          })),
        },
      })
      .code(200);

    return response;
  }

  // Terdapat query reading
  if (reading) {
    const filteredBooksReading = items.filter(
      (book) => Number(book.reading) === Number(reading),
    );

    const response = h
      .response({
        status: 'success',
        data: {
          book: filteredBooksReading.map((book) => ({
            id: book.id,
            name: book.name,
            publisher: book.publisher,
          })),
        },
      })
      .code(200);

    return response;
  }

  // Terdapat query finished
  if (finished) {
    const filteredBooksFinished = items.filter(
      (book) => Number(book.finished) === Number(finished),
    );

    const response = h
      .response({
        status: 'success',
        data: {
          books: filteredBooksFinished.map((book) => ({
            id: book.id,
            name: book.name,
            publisher: book.publisher,
          })),
        },
      })
      .code(200);

    return response;
  }

  // Tidak terdapat query apapun
  const response = h
    .response({
      status: 'success',
      data: {
        books: items.map((book) => ({
          id: book.id,
          name: book.name,
          publisher: book.publisher,
        })),
      },
    })
    .code(200);

  return response;
};

28. Untuk mengetes API handler tersebut, jalankan perintah berikut.

curl -X GET -H "Content-Type: application/json" http://localhost:9000/books?name=Dimas

29. Lengkapi getItemByIdHandler.

const getItemByIdHandler = (request, h) => {
  const { bookId } = request.params;
  const book = items.filter((note) => note.id === bookId)[0];

  // Jika buku dengan id yang dicari ditemukan
  if (book) {
    const response = h
      .response({
        status: 'success',
        data: {
          book,
        },
      })
      .code(200);
    return response;
  }

  // Jika buku dengan id yang dicari tidak ditemukan
  const response = h
    .response({
      status: 'fail',
      message: 'Buku tidak ditemukan',
    })
    .code(404);
  return response;
};

30. Lengkapi editItemByIdHandler. Method array findIndex() digunakan untuk mendapatkan index array pada objek. Jika note dengan id yang dicari ditemukan, maka index akan bernilai array index dari objek catatan yang dicari. Namun bila tidak ditemukan, maka index bernilai -1. Spread operator (…) digunakan untuk mempertahankan nilai items[index] yang tidak perlu diubah.

const editItemByIdHandler = (request, h) => {
  const { bookId } = request.params;

  const {
    name,
    year,
    author,
    summary,
    publisher,
    pageCount,
    readPage,
    reading,
  } = request.payload;

  // Tidak terdapat properti name pada request body
  if (!name) {
    const response = h
      .response({
        status: 'fail',
        message: 'Gagal memperbarui buku. Mohon isi nama buku',
      })
      .code(400);
    return response;
  }

  // Nilai properti readPage lebih besar dari pageCount pada request body
  if (readPage > pageCount) {
    const response = h
      .response({
        status: 'fail',
        message:
          'Gagal memperbarui buku. readPage tidak boleh lebih besar dari pageCount',
      })
      .code(400);
    return response;
  }

  const finished = pageCount === readPage;
  const updatedAt = new Date().toISOString();

  const index = items.findIndex((book) => book.id === bookId);

  // Jika book dengan id yang dicari ditemukan
  if (index !== -1) {
    items[index] = {
      ...items[index],
      name,
      year,
      author,
      summary,
      publisher,
      pageCount,
      readPage,
      reading,
      finished,
      updatedAt,
    };

    const response = h
      .response({
        status: 'success',
        message: 'Buku berhasil diperbarui',
      })
      .code(200);
    return response;
  }

  // Jika book dengan id yang dicari tidak ditemukan
  const response = h
    .response({
      status: 'fail',
      message: 'Gagal memperbarui buku. Id tidak ditemukan',
    })
    .code(404);
  return response;
};

31. Lengkapi deleteItemByIdHandler.

const deleteItemByIdHandler = (request, h) => {
  const { bookId } = request.params;

  const index = items.findIndex((book) => book.id === bookId);

  // Jika book dengan id yang dicari ditemukan
  if (index !== -1) {
    items.splice(index, 1);

    const response = h
      .response({
        status: 'success',
        message: 'Buku berhasil dihapus',
      })
      .code(200);
    return response;
  }

  // Jika book dengan id yang dicari tidak ditemukan
  const response = h
    .response({
      status: 'fail',
      message: 'Buku gagal dihapus. Id tidak ditemukan',
    })
    .code(404);
  return response;
};

Selesai!

GitHub Link: Click Here

Sekian Dicoding Submission Bookshelf API. Semoga Dicoding Submission Bookshelf API tadi dapat membantu teman-teman dalam belajar.

Referensi: https://en.wikipedia.org/wiki/Main_Page
Baca juga: 

Ambiz Education Search: