├── Procfile ├── .gitignore ├── HttpError.js ├── package.json ├── Cookie.js ├── Type.js ├── gen.js ├── test.html ├── CSV.js ├── data.json ├── README.md ├── index.html └── index.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node index 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log 3 | .DS_Store 4 | /*.env 5 | -------------------------------------------------------------------------------- /HttpError.js: -------------------------------------------------------------------------------- 1 | module.exports = function(method, message) { 2 | var error = new Error(message) 3 | error.method = method 4 | return error 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rem", 3 | "version": "1.0.0", 4 | "description": "A starting point for big dreams.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index" 8 | }, 9 | "keywords": ["REST", "API"], 10 | "author": "Leo Horie", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /Cookie.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parse: function(cookie) { 3 | var data = {} 4 | if (cookie) { 5 | cookie.split(";").forEach(function(c) { 6 | var parts = c.split("=") 7 | var key = parts.shift().trim() 8 | if (key !== "Path" && key !== "Domain" && key !== "Expires" && key !== "Secure" && key !== "HttpOnly") { 9 | data[key] = decodeURIComponent(parts.join("=")) 10 | } 11 | }) 12 | } 13 | return data 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Type.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | encode: function(list) { 3 | return list.map(function(item) { 4 | var obj = {} 5 | Object.keys(item).forEach(function(key) { 6 | obj[key] = JSON.stringify(item[key]) 7 | }) 8 | return obj 9 | }) 10 | }, 11 | decode: function(list) { 12 | return list.map(function(item) { 13 | var obj = {} 14 | Object.keys(item).forEach(function(key) { 15 | obj[key] = item[key] !== "" ? JSON.parse(item[key]) : undefined 16 | }) 17 | return obj 18 | }) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /gen.js: -------------------------------------------------------------------------------- 1 | var first = ["Peter", "Cindy", "Ted", "Susan", "Emily"] 2 | var last = ["Mackenzie", "Zhang", "Smith", "Fernbrook", "Kim"] 3 | 4 | var users = [], id = 1 5 | for (var i = 0; i < last.length; i++) { 6 | for (var j = 0; j < first.length; j++) { 7 | var f = first[j] 8 | var l = last[j + i < first.length ? j + i : j + i - first.length] 9 | users.push({id: id++, firstName: f, lastName: l/*, email: (f + l + "@mailinator.com").toLowerCase()*/}) 10 | } 11 | } 12 | 13 | var data = {users: users} 14 | 15 | require("fs").writeFileSync("data.json", JSON.stringify(data, null, 4), "utf-8") 16 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 43 | -------------------------------------------------------------------------------- /CSV.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parse: function parseCSV(csv) { 3 | var data = [], keys = [], header = true 4 | var i = 0, start = 0, col = 0, item = {} 5 | while (i < csv.length) { 6 | if (csv[i] === "\"") { 7 | start = i + 1 8 | while (++i) { 9 | if (csv[i] === "\"") break 10 | if (csv[i] === "\\") i += 1 11 | } 12 | var value = csv.substring(start, i).replace(/\\(.)/g, "$1") 13 | if (header) keys.push(value) 14 | else item[keys[col]] = value 15 | i++ 16 | continue 17 | } 18 | if (csv[i] === ",") { 19 | var value = csv.substring(start, i) 20 | if (header) keys.push(value) 21 | else item[keys[col]] = item[keys[col]] != null ? item[keys[col]] : value 22 | start = ++i 23 | col++ 24 | continue 25 | } 26 | if (csv[i] === "\n") { 27 | var value = csv.substring(start, i) 28 | start = ++i 29 | if (header) { 30 | keys.push(value) 31 | header = false 32 | } 33 | else { 34 | item[keys[col]] = item[keys[col]] != null ? item[keys[col]] : value 35 | data.push(item) 36 | item = {} 37 | } 38 | col = 0 39 | continue 40 | } 41 | i++ 42 | } 43 | if (Object.keys(item).length > 0) { 44 | var value = csv.substring(start, i) 45 | item[keys[col]] = item[keys[col]] != null ? item[keys[col]] : value 46 | data.push(item) 47 | } 48 | return data 49 | }, 50 | create: function(list) { 51 | var data = "" 52 | for (var k in list[0]) { 53 | data += k + "," 54 | } 55 | data = data.slice(0, -1) + "\n" 56 | 57 | for (var i = 0; i < list.length; i++) { 58 | for (var k in list[0]) { 59 | var value = list[i][k] !== undefined ? String(list[i][k]) : "" 60 | data += value.indexOf("\"") > -1 ? "\"" + value.replace(/("|,)/g, "\\$1") + "\"," : value + "," 61 | } 62 | data = data.slice(0, -1) + "\n" 63 | } 64 | data = data.slice(0, -1) 65 | return data 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "id": 1, 5 | "firstName": "Peter", 6 | "lastName": "Mackenzie" 7 | }, 8 | { 9 | "id": 2, 10 | "firstName": "Cindy", 11 | "lastName": "Zhang" 12 | }, 13 | { 14 | "id": 3, 15 | "firstName": "Ted", 16 | "lastName": "Smith" 17 | }, 18 | { 19 | "id": 4, 20 | "firstName": "Susan", 21 | "lastName": "Fernbrook" 22 | }, 23 | { 24 | "id": 5, 25 | "firstName": "Emily", 26 | "lastName": "Kim" 27 | }, 28 | { 29 | "id": 6, 30 | "firstName": "Peter", 31 | "lastName": "Zhang" 32 | }, 33 | { 34 | "id": 7, 35 | "firstName": "Cindy", 36 | "lastName": "Smith" 37 | }, 38 | { 39 | "id": 8, 40 | "firstName": "Ted", 41 | "lastName": "Fernbrook" 42 | }, 43 | { 44 | "id": 9, 45 | "firstName": "Susan", 46 | "lastName": "Kim" 47 | }, 48 | { 49 | "id": 10, 50 | "firstName": "Emily", 51 | "lastName": "Mackenzie" 52 | }, 53 | { 54 | "id": 11, 55 | "firstName": "Peter", 56 | "lastName": "Smith" 57 | }, 58 | { 59 | "id": 12, 60 | "firstName": "Cindy", 61 | "lastName": "Fernbrook" 62 | }, 63 | { 64 | "id": 13, 65 | "firstName": "Ted", 66 | "lastName": "Kim" 67 | }, 68 | { 69 | "id": 14, 70 | "firstName": "Susan", 71 | "lastName": "Mackenzie" 72 | }, 73 | { 74 | "id": 15, 75 | "firstName": "Emily", 76 | "lastName": "Zhang" 77 | }, 78 | { 79 | "id": 16, 80 | "firstName": "Peter", 81 | "lastName": "Fernbrook" 82 | }, 83 | { 84 | "id": 17, 85 | "firstName": "Cindy", 86 | "lastName": "Kim" 87 | }, 88 | { 89 | "id": 18, 90 | "firstName": "Ted", 91 | "lastName": "Mackenzie" 92 | }, 93 | { 94 | "id": 19, 95 | "firstName": "Susan", 96 | "lastName": "Zhang" 97 | }, 98 | { 99 | "id": 20, 100 | "firstName": "Emily", 101 | "lastName": "Smith" 102 | }, 103 | { 104 | "id": 21, 105 | "firstName": "Peter", 106 | "lastName": "Kim" 107 | }, 108 | { 109 | "id": 22, 110 | "firstName": "Cindy", 111 | "lastName": "Mackenzie" 112 | }, 113 | { 114 | "id": 23, 115 | "firstName": "Ted", 116 | "lastName": "Zhang" 117 | }, 118 | { 119 | "id": 24, 120 | "firstName": "Susan", 121 | "lastName": "Smith" 122 | }, 123 | { 124 | "id": 25, 125 | "firstName": "Emily", 126 | "lastName": "Fernbrook" 127 | } 128 | ] 129 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REM REST API 2 | 3 | [API](#api) | [What REM offers](#what-rem-offers) | [Why REM](#why-rem) | [How it works](#how-it-works) | [About](#about) | [Try it](http://rem-rest-api.herokuapp.com) 4 | 5 | A starting point for big dreams. 6 | 7 | REM is a REST API for prototyping. It accepts JSON requests, returns JSON responses and persists data between requests like a real API. But your test data is only visible to you. It's [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) enabled and no API key is required. 8 | 9 | ```javascript 10 | var xhr = new XMLHttpRequest() 11 | xhr.open("GET", "http://rem-rest-api.herokuapp.com/api/users", true) 12 | xhr.withCredentials = true 13 | xhr.send() 14 | xhr.onload = function() { 15 | var data = JSON.parse(xhr.responseText) 16 | } 17 | ``` 18 | 19 | --- 20 | 21 | ### API 22 | 23 | #### Get a list of things 24 | 25 | ``` 26 | GET http://rem-rest-api.herokuapp.com/api/users 27 | //or 28 | GET http://rem-rest-api.herokuapp.com/api/projects 29 | //or 30 | GET http://rem-rest-api.herokuapp.com/api/[whatever] 31 | ``` 32 | 33 | Results: 34 | 35 | ``` 36 | { 37 | "offset": 0, 38 | "limit": 10, 39 | "total": 36, 40 | "data": [{ 41 | "id": "1", 42 | "firstName": "Peter", 43 | "lastName": "Mackenzie", 44 | }, ...] 45 | } 46 | ``` 47 | 48 | Optional querystring parameters: 49 | 50 | - `offset`: pagination offset. Defaults to `0` 51 | - `limit`: pagination size. Defaults to `10` 52 | 53 | ``` 54 | GET http://rem-rest-api.herokuapp.com/api/[things]?offset=1&limit=10 55 | ``` 56 | 57 | --- 58 | 59 | #### Get one thing 60 | 61 | ``` 62 | GET http://rem-rest-api.herokuapp.com/api/[things]/1 63 | ``` 64 | 65 | Results: 66 | 67 | ``` 68 | { 69 | "id": "1", 70 | "firstName": "Peter", 71 | "lastName": "Mackenzie" 72 | } 73 | ``` 74 | 75 | --- 76 | 77 | #### Create new thing 78 | 79 | ``` 80 | POST http://rem-rest-api.herokuapp.com/api/[things] 81 | 82 | {"firstName": "Lorem", "lastName": "Ipsum"} 83 | ``` 84 | 85 | --- 86 | 87 | #### Upsert/replace thing 88 | 89 | ``` 90 | PUT http://rem-rest-api.herokuapp.com/api/[things]/1 91 | 92 | {"id": 1, "firstName": "Lorem", "lastName": "Ipsum"} 93 | ``` 94 | 95 | --- 96 | 97 | #### Delete thing 98 | 99 | ``` 100 | DELETE http://rem-rest-api.herokuapp.com/api/[things]/1 101 | ``` 102 | 103 | --- 104 | 105 | ### What REM offers 106 | 107 | The `/api/users` endpoint comes pre-populated with some dummy data. You can also post anything to any endpoint you want (e.g. `/api/projects`, `/api/comments`, `/api/toys`, and so on) to create datasets on the fly. Those start off empty. `POST` endpoints automatically add an `id` field to new entities. In the same vein, other endpoints expect the primary key field to be called `id`, so that request like `PUT /api/things/1` and `DELETE /api/others/2` can affect the correct entities. Other than that, there are no schema restrictions. 108 | 109 | The server has [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) enabled for all origins, and no API key is required. You can just make requests from anywhere and be on your way. 110 | 111 | --- 112 | 113 | ### Why REM 114 | 115 | Creating an application these days can often be paralyzing: Ruby or Python? Flask or Express? MySQL or Postgres? What about Redis? Often times, people give up before even starting. 116 | 117 | REM is useful if you want to put together a UI but don't want to set up a server and a database to store your data yet (or ever). Maybe you're making a demo, running a workshop, or test-driving a trendy javascript framework. Whatever it is, if you just want to get an idea on screen, this tool is for you. 118 | 119 | --- 120 | 121 | ### How it works 122 | 123 | The REM API responds as if it had a real database storing your data, but rather than doing that, it saves data in a cookie instead. 124 | 125 | This means you can create entities, update them and query them, and the data will persist between requests, just like in a real API. The difference vs a real API is that no one else can see your data, because nothing actually gets saved in the server. Any data you create is only available to you, and only until the end of the browser session. 126 | 127 | Note that cookies can only store about 4kb worth of data. If you go over the limit, the server will return an error and clear the cookie. When this happens, all the data will be erased, so you can continue using REM as a data storage mechanism without needing to mess around with the storage cookie. 128 | 129 | --- 130 | 131 | ### About 132 | 133 | REM is ~250 LOC, and has no NPM dependencies. 134 | 135 | Licence: MIT 136 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | REM - REST API 6 | 7 | 8 | 20 | 21 | 22 |
23 |

REM REST API

24 | 25 |

API | What REM offers | Why REM | How it works | About | Github

26 | 27 |

A starting point for big dreams.

28 | 29 |

REM is a REST API for prototyping. It accepts JSON requests, returns JSON responses and persists data between requests like a real API. But your test data is only visible to you. It's CORS enabled and no API key is required.

30 | 31 |
var xhr = new XMLHttpRequest()
 32 | xhr.open("GET", "http://rem-rest-api.herokuapp.com/api/users", true)
 33 | xhr.withCredentials = true
 34 | xhr.send()
 35 | xhr.onload = function() {
 36 |   var data = JSON.parse(xhr.responseText)
 37 | }
38 | 39 |
40 | 41 |

API

42 | 43 |

Get a list of things

44 | 45 |
GET http://rem-rest-api.herokuapp.com/api/[things]
46 | 47 |

Results:

48 | 49 |
{
 50 |   "offset": 0,
 51 |   "limit": 10,
 52 |   "total": 36,
 53 |   "data": [{
 54 |     "id": "1",
 55 |     "firstName": "Peter",
 56 |     "lastName": "Mackenzie",
 57 |   }, ...]
 58 | }
59 | 60 |

Optional querystring parameters:

61 | 62 | 67 | 68 |
GET http://rem-rest-api.herokuapp.com/api/[things]?offset=1&limit=10
69 | 70 |
71 | 72 |

Get one thing

73 | 74 |
GET http://rem-rest-api.herokuapp.com/api/[things]/1
75 | 76 |

Results:

77 | 78 |
{
 79 |   "id": "1",
 80 |   "firstName": "Peter",
 81 |   "lastName": "Mackenzie"
 82 | }
83 | 84 |
85 | 86 |

Create new thing

87 | 88 |
POST http://rem-rest-api.herokuapp.com/api/[things]
 89 | 
 90 | {"firstName": "Lorem", "lastName": "Ipsum"}
91 | 92 |
93 | 94 |

Upsert/replace thing

95 | 96 |
PUT http://rem-rest-api.herokuapp.com/api/[things]/1
 97 | 
 98 | {"id": 1, "firstName": "Lorem", "lastName": "Ipsum"}
99 | 100 |
101 | 102 |

Delete thing

103 | 104 |
DELETE http://rem-rest-api.herokuapp.com/api/[things]/1
105 | 106 |
107 | 108 |

What REM offers

109 | 110 |

The /api/users endpoint comes pre-populated with some dummy data. You can also post anything to any endpoint you want (e.g. /api/projects, /api/comments, /api/toys, and so on) to create datasets on the fly. Those start off empty. POST endpoints automatically add an id field to new entities. In the same vein, other endpoints expect the primary key field to be called id, so that request like PUT /api/things/1 and DELETE /api/others/2 can affect the correct entities. Other than that, there are no schema restrictions.

111 | 112 |

The server has CORS enabled for all origins, and no API key is required. You can just make requests from anywhere and be on your way.

113 | 114 |
115 | 116 |

Why REM

117 | 118 |

Creating an application these days can often be paralyzing: Ruby or Python? Flask or Express? MySQL or Postgres? What about Redis? Often times, people give up before even starting.

119 | 120 |

REM is useful if you want to put together a UI but don't want to set up a server and a database to store your data yet (or ever). Maybe you're making a demo, running a workshop, or test-driving a trendy javascript framework. Whatever it is, if you just want to get an idea on screen, this tool is for you.

121 | 122 |
123 | 124 |

How it works

125 | 126 |

The REM API responds as if it had a real database storing your data, but rather than doing that, it saves data in a cookie instead.

127 | 128 |

This means you can create entities, update them and query them, and the data will persist between requests, just like in a real API. The difference vs a real API is that no one else can see your data, because nothing actually gets saved in the server. Any data you create is only available to you, and only until the end of the browser session.

129 | 130 |

Note that cookies can only store about 4kb worth of data. If you go over the limit, the server will return an error and clear the cookie. When this happens, all the data will be erased, so you can continue using REM as a data storage mechanism without needing to mess around with the storage cookie.

131 | 132 |
133 | 134 |

About

135 | 136 |

REM is ~250 LOC, and has no NPM dependencies.

137 | 138 |

Licence: MIT

139 |
140 | 141 | 142 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require("http") 2 | var url = require("url") 3 | var querystring = require("querystring") 4 | 5 | var CSV = require("./CSV") 6 | var Type = require("./Type") 7 | var Cookie = require("./Cookie") 8 | var HttpError = require("./HttpError") 9 | 10 | var home = require("fs").readFileSync("index.html", "utf-8") 11 | var data = require("./data.json") 12 | 13 | http.createServer(function route(req, res) { 14 | var u = url.parse(req.url) 15 | var q = u.search ? querystring.parse(u.search.slice(1)) : {} 16 | var args = u.pathname.match(/\/(api)\/([^\/]+)(?:\/([^\/]+))?/) // `/api/:collection/:id` 17 | try { 18 | if (args == null) { 19 | if (u.pathname === "/") { 20 | res.writeHead(200) 21 | res.end(home) 22 | return 23 | } 24 | else throw new HttpError(404, "Not found") 25 | } 26 | var resStatus = req.headers['rem-response-status'] 27 | if (resStatus) throw new HttpError(Number(resStatus), "Rem-Response-Status") 28 | if (args[1] !== "api") throw new HttpError(404, "Not found") 29 | var key = args[2] 30 | var id = Number(args[3]) 31 | var db = getData(req.headers.cookie) 32 | var items = db[key] || [] 33 | req.method = req.method.toUpperCase() 34 | if (req.method === "GET") { 35 | res.writeHead(200, { 36 | "Content-Type": "application/json", 37 | "Access-Control-Allow-Origin": req.headers.origin || '', 38 | "Access-Control-Allow-Credentials": "true", 39 | }) 40 | var offset = isNaN(parseInt(q.offset, 10)) ? 0 : parseInt(q.offset, 10) 41 | var limit = isNaN(parseInt(q.limit, 10)) ? 10 : parseInt(q.limit, 10) 42 | var payload = get(id, items) 43 | var output = payload instanceof Array ? { 44 | data: payload.slice(offset, offset + limit), 45 | offset: offset, 46 | limit: limit, 47 | total: items.length, 48 | } : payload 49 | res.end(JSON.stringify(output, null, 2)) 50 | } 51 | else if (req.method === "PUT" || req.method === "POST" || req.method === "DELETE") { 52 | var body = "" 53 | req.on("data",function(data) { 54 | body += data.toString() 55 | }) 56 | req.on("end",function commit() { 57 | try { 58 | var item = body !== "" ? JSON.parse(body) : null 59 | if (req.method === "DELETE") remove(id, items) 60 | else if (item != null) { 61 | if (req.method === "PUT") put(id, items, item) 62 | if (req.method === "POST") post(id, items, item) 63 | } 64 | else throw new HttpError(400, "Missing JSON input") 65 | db[key] = items 66 | var output = stageData(db) 67 | if (output.length <= 4093) { 68 | res.writeHead(200, { 69 | "Content-Type": "application/json", 70 | "Set-Cookie": output, 71 | "Access-Control-Allow-Origin": req.headers.origin || '', 72 | "Access-Control-Allow-Credentials": "true", 73 | }) 74 | res.end(JSON.stringify(item, null, 2)) 75 | } 76 | else { 77 | var error = new HttpError(500, "Database size limit exceeded! Clearing data") 78 | for (var k in db) db[k] = [] 79 | res.writeHead(500, { 80 | "Content-Type": "application/json", 81 | "Set-Cookie": output, 82 | "Access-Control-Allow-Origin": req.headers.origin || '', 83 | "Access-Control-Allow-Credentials": "true", 84 | }) 85 | res.end(JSON.stringify({message: error.message, stack: e.stack}, null, 2)) 86 | } 87 | } 88 | catch (e) { 89 | console.log(e) 90 | res.writeHead((e && e.method) || 400, { 91 | "Content-Type": "application/json", 92 | "Access-Control-Allow-Origin": req.headers.origin || '', 93 | "Access-Control-Allow-Credentials": "true", 94 | }) 95 | res.end(JSON.stringify({message: e.message, stack: e.stack}, null, 2)) 96 | } 97 | }) 98 | } 99 | else if (req.method === "OPTIONS") { 100 | res.writeHead(200, { 101 | "Access-Control-Allow-Origin": req.headers.origin || '', 102 | "Access-Control-Allow-Credentials": "true", 103 | "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS", 104 | "Access-Control-Allow-Headers": "Content-Type,Rem-Response-Status", 105 | "Access-Control-Max-Age": 600, // seconds 106 | }) 107 | res.end("") 108 | } 109 | else throw new HttpError(405, "Method not allowed") 110 | } 111 | catch (e) { 112 | res.writeHead(e.method || 400, { 113 | "Content-Type": "application/json", 114 | "Access-Control-Allow-Origin": req.headers.origin || '', 115 | "Access-Control-Allow-Credentials": "true", 116 | }) 117 | res.end(JSON.stringify({message: e.message, stack: e.stack}, null, 2)) 118 | } 119 | }).listen(process.env.PORT || 8000) 120 | 121 | function getData(cookieString) { 122 | var map = Cookie.parse(cookieString) 123 | if (Object.keys(map).length > 0) { 124 | for (var k in map) { 125 | var csv = CSV.parse(map[k]) 126 | map[k] = Type.decode(csv) 127 | } 128 | return map 129 | } 130 | return JSON.parse(JSON.stringify(data)) 131 | } 132 | function stageData(db) { 133 | var cookies = [] 134 | for (var k in db) { 135 | var typed = Type.encode(db[k]) 136 | var csv = CSV.create(typed) 137 | cookies.push(k + "=" + encodeURIComponent(csv) + "; Path=/;") 138 | } 139 | return cookies 140 | } 141 | function get(id, items) { 142 | if (!id) return items 143 | for (var i = 0; i < items.length; i++) { 144 | if (items[i].id === id) return items[i] 145 | } 146 | throw new HttpError(404, "Item not found") 147 | } 148 | function put(id, items, item) { 149 | if (!id) throw new HttpError(400, "ID must be provided") 150 | item.id = id 151 | var found = false 152 | for (var i = 0; i < items.length; i++) { 153 | if (items[i].id === id) { 154 | items[i] = item 155 | found = true 156 | break 157 | } 158 | } 159 | if (!found) items.push(item) 160 | } 161 | function post(id, items, item) { 162 | if (id) throw new HttpError(400, "Cannot post with ID") 163 | var last = items.slice().sort(function(a, b) {return b.id - a.id})[0] 164 | var newId = last ? last.id : 0 165 | item.id = Number(newId) + 1 166 | items.push(item) 167 | } 168 | function remove(id, items) { 169 | if (!id) throw new HttpError(400, "ID must be provided") 170 | for (var i = 0; i < items.length; i++) { 171 | if (items[i].id === id) items.splice(i, 1) 172 | } 173 | } 174 | --------------------------------------------------------------------------------