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]
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 |
--------------------------------------------------------------------------------