├── Procfile ├── .gitignore ├── src ├── books.js ├── server.js ├── routes.js └── handler.js ├── .eslintrc.json ├── package.json └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node ./src/server.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | notes-api-webserver.pem -------------------------------------------------------------------------------- /src/books.js: -------------------------------------------------------------------------------- 1 | const books = []; 2 | 3 | module.exports = books; 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb-base"], 8 | "parserOptions": { 9 | "ecmaVersion": 12 10 | }, 11 | "rules": { 12 | "no-console": "off", 13 | "linebreak-style": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('@hapi/hapi'); 2 | // files 3 | const routes = require('./routes'); 4 | 5 | const init = async () => { 6 | const server = Hapi.server({ 7 | port: 5000, 8 | host: 9 | process.env.NODE_ENV === 'development' 10 | ? 'localhost' 11 | : 'dicoding-bookshelf.herokuapp.com', 12 | routes: { 13 | cors: { 14 | origin: ['*'], 15 | }, 16 | }, 17 | }); 18 | 19 | server.route(routes); 20 | 21 | await server.start(); 22 | console.log(`Server berjalan pada ${server.info.uri}`); 23 | }; 24 | 25 | init(); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notes-app-web-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/server.js", 6 | "scripts": { 7 | "start": "node ./src/server.js", 8 | "start-dev": "nodemon ./src/server.js", 9 | "start-prod": "NODE_ENV=production node ./src/server.js", 10 | "lint": "eslint ./src" 11 | }, 12 | "engines": { 13 | "node": "12.x" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "eslint": "^7.23.0", 20 | "eslint-config-airbnb-base": "^14.2.1", 21 | "eslint-plugin-import": "^2.22.1", 22 | "nodemon": "^2.0.7" 23 | }, 24 | "dependencies": { 25 | "@hapi/hapi": "^20.1.2", 26 | "nanoid": "^3.1.22" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📃 Description 2 | 3 | Projek ini adalah submission dari Dicoding untuk kelas Belajar Membuat Aplikasi Back-End untuk Pemula. Kelas ini memiliki 1 submission yang harus diselesaikan untuk mendapatkan sertifikat. 4 | 5 | Submission 1 dengan kriteria fitur: 6 | 7 | - Proyek Bookshelf API harus memenuhi seluruh pengujian otomatis pada Postman request yang bertanda **Mandatory**. Bila salah satu pengujiannya gagal, maka proyek Anda akan kami tolak, 8 | - Tambahkan fitur query parameters `?name`, `?reading` dan `?finished` pada route GET /books, 9 | - Menerapkan CORS pada seluruh resource yang ada, 10 | - Menggunakan ESLint dan salah satu style guide agar gaya penulisan kode JavaScript lebih konsisten, 11 | 12 | > Submission 1 diharuskan menggunakan bahasa Javascript NodeJS dengan framework Hapi. 13 | 14 | ## 👨🏻‍💻 Tech Stack 15 | 16 | Backend menggunakan library @hapi/hapi, eslint, dan nanoid. 17 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | const { 2 | addBookHandler, 3 | getAllBooksHandler, 4 | getBookByIdHandler, 5 | editBookByIdHandler, 6 | deleteBookByIdHandler, 7 | } = require('./handler'); 8 | 9 | const routes = [ 10 | { 11 | method: 'POST', 12 | path: '/books', 13 | handler: addBookHandler, 14 | }, 15 | { 16 | method: 'GET', 17 | path: '/books', 18 | handler: getAllBooksHandler, 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/books/{bookId}', 23 | handler: getBookByIdHandler, 24 | }, 25 | { 26 | method: 'PUT', 27 | path: '/books/{bookId}', 28 | handler: editBookByIdHandler, 29 | }, 30 | { 31 | method: 'DELETE', 32 | path: '/books/{bookId}', 33 | handler: deleteBookByIdHandler, 34 | }, 35 | { 36 | method: '*', 37 | path: '/{any*}', 38 | handler: () => 'Halaman tidak ditemukan', 39 | }, 40 | ]; 41 | 42 | module.exports = routes; 43 | -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | const { nanoid } = require('nanoid'); 2 | // files 3 | const books = require('./books'); 4 | 5 | const addBookHandler = (request, h) => { 6 | const { 7 | name, 8 | year, 9 | author, 10 | summary, 11 | publisher, 12 | pageCount, 13 | readPage, 14 | reading, 15 | } = request.payload; 16 | 17 | if (!name) { 18 | // Client tidak melampirkan properti name pada request body 19 | const response = h 20 | .response({ 21 | status: 'fail', 22 | message: 'Gagal menambahkan buku. Mohon isi nama buku', 23 | }) 24 | .code(400); 25 | return response; 26 | } 27 | 28 | if (readPage > pageCount) { 29 | // Client melampirkan nilai properti readPage yang lebih besar dari nilai properti pageCount 30 | const response = h 31 | .response({ 32 | status: 'fail', 33 | message: 34 | 'Gagal menambahkan buku. readPage tidak boleh lebih besar dari pageCount', 35 | }) 36 | .code(400); 37 | return response; 38 | } 39 | 40 | const id = nanoid(16); 41 | const finished = pageCount === readPage; 42 | const insertedAt = new Date().toISOString(); 43 | const updatedAt = insertedAt; 44 | 45 | const newBook = { 46 | name, 47 | year, 48 | author, 49 | summary, 50 | publisher, 51 | pageCount, 52 | readPage, 53 | reading, 54 | id, 55 | finished, 56 | insertedAt, 57 | updatedAt, 58 | }; 59 | 60 | books.push(newBook); // push to books array 61 | 62 | const isSuccess = books.filter((note) => note.id === id).length > 0; // cek if newBook pushed 63 | 64 | if (isSuccess) { 65 | // Bila buku berhasil dimasukkan 66 | const response = h 67 | .response({ 68 | status: 'success', 69 | message: 'Buku berhasil ditambahkan', 70 | data: { 71 | bookId: id, 72 | }, 73 | }) 74 | .code(201); 75 | return response; 76 | } 77 | 78 | // Server gagal memasukkan buku karena alasan umum (generic error). 79 | const response = h 80 | .response({ 81 | status: 'fail', 82 | message: 'Buku gagal ditambahkan', 83 | }) 84 | .code(500); 85 | return response; 86 | }; 87 | 88 | const getAllBooksHandler = (request, h) => { 89 | const { name, reading, finished } = request.query; 90 | 91 | if (!name && !reading && !finished) { 92 | // kalau tidak ada query 93 | const response = h 94 | .response({ 95 | status: 'success', 96 | data: { 97 | books: books.map((book) => ({ 98 | id: book.id, 99 | name: book.name, 100 | publisher: book.publisher, 101 | })), 102 | }, 103 | }) 104 | .code(200); 105 | 106 | return response; 107 | } 108 | 109 | if (name) { 110 | const filteredBooksName = books.filter((book) => { 111 | // kalau ada query name 112 | const nameRegex = new RegExp(name, 'gi'); 113 | return nameRegex.test(book.name); 114 | }); 115 | 116 | const response = h 117 | .response({ 118 | status: 'success', 119 | data: { 120 | books: filteredBooksName.map((book) => ({ 121 | id: book.id, 122 | name: book.name, 123 | publisher: book.publisher, 124 | })), 125 | }, 126 | }) 127 | .code(200); 128 | 129 | return response; 130 | } 131 | 132 | if (reading) { 133 | // kalau ada query reading 134 | const filteredBooksReading = books.filter( 135 | (book) => Number(book.reading) === Number(reading), 136 | ); 137 | 138 | const response = h 139 | .response({ 140 | status: 'success', 141 | data: { 142 | books: filteredBooksReading.map((book) => ({ 143 | id: book.id, 144 | name: book.name, 145 | publisher: book.publisher, 146 | })), 147 | }, 148 | }) 149 | .code(200); 150 | 151 | return response; 152 | } 153 | 154 | // kalau ada query finished 155 | const filteredBooksFinished = books.filter( 156 | (book) => Number(book.finished) === Number(finished), 157 | ); 158 | 159 | const response = h 160 | .response({ 161 | status: 'success', 162 | data: { 163 | books: filteredBooksFinished.map((book) => ({ 164 | id: book.id, 165 | name: book.name, 166 | publisher: book.publisher, 167 | })), 168 | }, 169 | }) 170 | .code(200); 171 | 172 | return response; 173 | }; 174 | 175 | const getBookByIdHandler = (request, h) => { 176 | const { bookId } = request.params; 177 | 178 | const book = books.filter((n) => n.id === bookId)[0]; // find book by id 179 | 180 | if (book) { 181 | // Bila buku dengan id yang dilampirkan ditemukan 182 | const response = h 183 | .response({ 184 | status: 'success', 185 | data: { 186 | book, 187 | }, 188 | }) 189 | .code(200); 190 | return response; 191 | } 192 | 193 | // Bila buku dengan id yang dilampirkan oleh client tidak ditemukan 194 | const response = h 195 | .response({ 196 | status: 'fail', 197 | message: 'Buku tidak ditemukan', 198 | }) 199 | .code(404); 200 | return response; 201 | }; 202 | 203 | const editBookByIdHandler = (request, h) => { 204 | const { bookId } = request.params; 205 | 206 | const { 207 | name, 208 | year, 209 | author, 210 | summary, 211 | publisher, 212 | pageCount, 213 | readPage, 214 | reading, 215 | } = request.payload; 216 | 217 | if (!name) { 218 | // Client tidak melampirkan properti name pada request body 219 | const response = h 220 | .response({ 221 | status: 'fail', 222 | message: 'Gagal memperbarui buku. Mohon isi nama buku', 223 | }) 224 | .code(400); 225 | return response; 226 | } 227 | 228 | if (readPage > pageCount) { 229 | // Client melampirkan nilai properti readPage yang lebih besar dari nilai properti pageCount 230 | const response = h 231 | .response({ 232 | status: 'fail', 233 | message: 234 | 'Gagal memperbarui buku. readPage tidak boleh lebih besar dari pageCount', 235 | }) 236 | .code(400); 237 | return response; 238 | } 239 | 240 | const finished = pageCount === readPage; 241 | const updatedAt = new Date().toISOString(); 242 | 243 | const index = books.findIndex((note) => note.id === bookId); // find book by id 244 | 245 | if (index !== -1) { 246 | books[index] = { 247 | ...books[index], 248 | name, 249 | year, 250 | author, 251 | summary, 252 | publisher, 253 | pageCount, 254 | readPage, 255 | reading, 256 | finished, 257 | updatedAt, 258 | }; 259 | 260 | // Bila buku berhasil diperbarui 261 | const response = h 262 | .response({ 263 | status: 'success', 264 | message: 'Buku berhasil diperbarui', 265 | }) 266 | .code(200); 267 | return response; 268 | } 269 | 270 | // id yang dilampirkan oleh client tidak ditemukkan oleh server 271 | const response = h 272 | .response({ 273 | status: 'fail', 274 | message: 'Gagal memperbarui buku. Id tidak ditemukan', 275 | }) 276 | .code(404); 277 | return response; 278 | }; 279 | 280 | const deleteBookByIdHandler = (request, h) => { 281 | const { bookId } = request.params; 282 | 283 | const index = books.findIndex((note) => note.id === bookId); // find book by id 284 | 285 | if (index !== -1) { 286 | books.splice(index, 1); 287 | 288 | // Bila id dimiliki oleh salah satu buku 289 | const response = h 290 | .response({ 291 | status: 'success', 292 | message: 'Buku berhasil dihapus', 293 | }) 294 | .code(200); 295 | return response; 296 | } 297 | 298 | // Bila id yang dilampirkan tidak dimiliki oleh buku manapun 299 | const response = h 300 | .response({ 301 | status: 'fail', 302 | message: 'Buku gagal dihapus. Id tidak ditemukan', 303 | }) 304 | .code(404); 305 | return response; 306 | }; 307 | 308 | module.exports = { 309 | addBookHandler, 310 | getAllBooksHandler, 311 | getBookByIdHandler, 312 | editBookByIdHandler, 313 | deleteBookByIdHandler, 314 | }; 315 | --------------------------------------------------------------------------------