rest-on-mongo is a REST API layer over MongoDB.
The need for a REST API interface to MongoDB came up when we wanted to access our MongoDB database directly from Google Sheets. The solutions out there were either using Stitch (a service provided by a hosted version of MongoDB) or using tools no longer maintained, or tools that did not support bulk updates and multiple databases. None of them worked for us. And we felt that this was such a common need that there had to be an open-source package for it.
So we built it.
v2.0 has breaking changes in the way BSON data types are handled. We no longer support automatic conversion of date-like strings to Date objects. The v1.0 functions for converting strings starting with a $
character to specific data types are also no longer supported.
The API is really REST-ish and not pure REST. The verbs supported are a little more than what pure REST would recommend. But the extra support is quite convenient as we found out.
npm install rest-on-mongo
There are two ways to use rest-on-mongo. You can just run the built-in server (which should work for most needs, because it has lots of configuration options), or you can use it as a library and build your own server.
After installation, just run:
npx rest-on-mongo
You will now have a full-fledged REST API server, using the default configuration: an unauthenticated server on port 8000 which will give you access to a database called test
in a MongoDB instance running on the localhost.
Insert a document in a collection called example
in the database test
:
curl -X POST --data '{ "_id": 1, "value": "some value" }' http://localhost:8000/example
Read back the document from the collection:
curl http://localhost:8000/example/1
This will return the document { "_id": 1, "value": "some value" }
which was just inserted.
The server can be started in one of the following modes:
- Single database (as in the above example), which can be accessed like
http://localhost:8000/collection/_id
- Multiple databases, each under a prefix in the route, accessed like
http://localhost:8000/prefix/collection/_id
- Multiple servers and databases in a hierarchy of roures accessed like
http://localhost:8000/server-name/database/collection/_id
All the configuration is in environment variables. You can set these using export VAR=value
or, write the definitions in a file called .env
since we use dotenv.
Variable name | DBs | Default | Notes |
---|---|---|---|
AUTH_TOKEN |
Any | (No auth) | Authentication token (or API Key) |
BASE |
Any | / | The base endpoint, e.g., /api/v1 |
MAX_BODY_SIZE |
Any | 10mb | limit for body-parser |
PORT |
Any | 8000 | HTTP Port to start the server on |
READ_ONLY |
Any | no | Exposes only GET methods |
SERVER |
Any | mongodb://localhost |
MongoDB connection URI |
DB |
Single | test |
Name of the one and only database |
PREFIXES |
Multiple | A comma separated list of prefixes | |
READ_ONLY_<prefix> |
Multiple | READ_ONLY |
Overrides READ_ONLY per <prefix> |
SERVER_<prefix> |
Multiple | SERVER |
Overrides SERVER per <prefix> |
DB_<prefix> |
Multiple | <prefix> |
Name of the database for <prefix> |
Note that prefixes can also be of the form server-name/db
, in which case the <prefix>
for all SERVER
env variables will become the server
part and that for the DB
ones will become the db
part. If the prefixes are of the form a,b,c*x,y,z
it will be expanded to a cartesian product like a/x
, a/y
etc.
Contents of the .env
file:
AUTH_TOKEN=secret
PORT=3000
PREFIXES=localtest,localcontent,remotetest,remotecontent
# SERVER_localtest=mongodb://localhost
# DB_localtest=test
# SERVER_localcontent=mongodb://localhost
DB_localcontent=content
SERVER_remotetest=mongodb://user:password@db.example.com
# DB_remotetest=test
SERVER_remotecontent=mongodb://user:password@db.example.com
DB_remotecontent=content
This will configure a server in the multiple databases mode. Now, to access the example
collection in the local test
database, you will need to do the following:
curl -H 'Authorization: Bearer secret' http://localhost:3000/local-test/example/1
And, to access the same collection in the content
database in the remote server, you would do:
curl -H 'Authorization: Bearer secret' http://localhost:3000/remote-content/example/1
SERVER=mongodb://user:password@db.example.com
DB=content
BASE=/api/v1
READ_ONLY=yes
This will configure a server with no authentication in the single database mode, so no prefix is needed to select the database. But a standard base prefix api/v1
is needed for all APIs. Further, HTTP methods other than GET will not be supported because READ_ONLY
was set to yes
.
In this mode, to get a document, this is what you would do:
curl http://localhost:8000/api/v1/example/1
SERVER=mongodb://user:password@db.example.com
PREFIXES=hr,sales
DB_hr=humanresources
# DB_sales=sales
This will configure a server with no authentication in the multiple database mode, with a single server. The DB_xxx
environment variable is not required since it matches the prefix, whereas the DB_hr
variable is required.
PREFIXES=prod/sales,prod/warehouse,staging/sales,staging/warehouse,local/sales,local/warehouse
# Can also be written as PREFIXES=prod,staging,local*sales,warehouse
SERVER_prod=mongodb+srv://user:password@cluster0-xxxx.mongodb.net
SERVER_staging=mongodb://user:password@db.example.com
# SERVER_local=localhost
READ_ONLY_prod_sales=yes
In this mode, this is what you would do to fetch an invoice document from the sales database in the production server:
curl http://localhost:8000/prod/sales/invoices/INV0001
- Multiple prefixes can connect to the same server+database. Although supported, this is useless.
- None of the environment variables are required. A server as in the Quick Start section above will be started if no environment variables are found.
- Since the shell does not allow usage of special characters (
-
,.
and/
) in environment variable names, you should use an_
instead of-
in the names where there is these characters exist in the prefix (which is indeed allowed).
You may want to create your own server if you need something different from rest-on-mongo's built-in server. For example, you may already have a server and you'd like an additional router endpoint. Or, you may not like the default authorization mechanism, and you want to use your own.
rest-on-mongo exports three things:
server
: The entire server, which can be used to start the server, or mount the middleware in your own app. Methods available areserver.start()
andserver.routes()
.restRoutes
: The REST routes, a lower level access to the REST handlers. Methods available arerestRoutes.all()
andrestRoutes.readOnly()
.tokenAuth
: An authentication middleware generator function. Pass a string to this function to obtain a function that authenticates against this string. An HTTPAuthorization
Header of typeBearer
will be expected in all requests.
To replicate the behaviour of the rest-on-mongo
command-line, you could do the following:
const { server } = require('rest-on-mongo');
server.start();
To mount the REST APIs in your own app (using the environment variables configuration), rather than start an independent server, you could do:
const { server } = require('rest-on-mongo');
const express = require('express');
const myApp = require('./app'); // Your app with its own handlers
const app = express();
app.use('/app', myApp); // mount your app on /app
app.use('/dbadmin', server.routes()); // mount rest-on-mongo on /dbadmin
To get an even lower level access to the REST routes, you could do:
const mongodb = require('mongodb');
const express = require('express');
const { restRoutes, tokenAuth } = require('rest-on-mongo');
const myApp = require('./app'); // Your app with its own handlers
const app = express();
app.use('/app', myApp); // mount your app on /app
// Authenticate using rest-on-mongo's default auth mechanism
app.use('/dbadmin', tokenAuth('somesecret'));
// Inject a database connection, this is required!
const client = new MongoClient('mongodb://db.example.com');
client.connect();
const db = client.db('mydatabase');
app.use('/dbadmin', (req, res, next) => {
req.db = db;
next();
}
// Install all the library's REST routes
app.use('/dbadmin', restRoutes.all()); // Or, use restRoutes.readOnly()
// Start the server
app.listen(8000, () => {
console.log(`API server started on port 8000`);
});
Method | Path | Request body | What it does |
---|---|---|---|
POST | /collection/_id | {...} |
Inserts a document at _id |
POST | /collection | {"_id": 1, ...} |
Inserts a document with the given _id |
{...} |
Inserts a document with an autogenerated _id |
||
[{...}, ...] |
Inserts many documents | ||
GET | /collection/_id | Gets a single document | |
GET | /collection | Gets documents matching the query-string filter | |
PATCH | /collection/_id | {...} |
Udates document at _id using MongoDB $set |
PATCH | /collection | [{"_id": 1, ...}, ...] |
Batch updates multiple documents |
PUT | /collection/_id | {...} |
Replaces document at _id |
PUT | /collection | [{"_id": 1, ...}, ...] |
Batch replaces multiple documents |
DELETE | /collection/_id | Deletes document at _id |
|
DELETE | /collection | Deletes objects matching the query-string filter |
The _id
parameter will be used as is, as a string, except in the following cases:
- The value happens to be a valid integer. In this case, parseInt will be used to convert it to an integer.
- The value happens to be in the
ObjectId
format. In this case,ObjectId()
will be used to convert it to a MongoDBObjectId
. - If the value starts with the single-quote character (
'
) the single-quote character will be stripped and the rest of the value will be used as a string. This is useful if you want to force an integer value to be used as a string.
- Except for the Create (POST) operation, When using the
collection/_id
way of identifying a document, if the document does not exist, an HTTP 404 (Not Found) error is returned. - For invalid requests (e.g., missing
_id
parameter in the request body), an HTTP 400 (Bad Request) error is returned. The response body will be a JSON with additional details.
Creates one or more objects. Specifying the primary key is optional, just like the MongoDB insert
operation. A primary key will be generated if not specified.
Variants:
collection/_id
,{}
: Inserts a document (body) with primary key_id
request body need not have it.collection
,{}
: When the request body is an object, a single document is inserted. If the document contains an_id
, it is used as the primary key, otherwise the primary key is generated just like the MongoDBinsert
command.collection
,[]
, request body is an array: Each document is inserted using MongoDBinsertMany
. The primary key,_id
, will be generated if not supplied.
Notes:
- These methods will not update or replace an existing document with
_id
, if it exists. In other words, this is not an upsert operation. The operation will fail with a 400 HTTP status. - In a bulk operation, if any supplied
_id
document exists, the entire operation will fail.
Success response:
{
"result" : {
"ok" : 1,
}
"insertedCount" : 2,
"insertedIds" : {
"0" : 1,
"1" : "5e635b1d7a189f6a101e67ad"
},
}
Failure response (in case of a duplicate key):
{
"status": "error",
"error": {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.test index: _id_ dup key: { _id: 1 }",
"index" : 0
}
}
Returns one ore more objects from a collection.
Variants:
collection/_id
: Gets the single document.collection
,[?<query>]
: We use query-params-mongo for parsing the filter specification. Note that this has sorting and pagination support too. For the filter, the MongoDB filter can be specified directly in a parameter named__filter
.
Returns:
- Success: A single object or an array of objects, either of which can be empty.
- Failure: In case of a single document request, a HTTP Status 404 is is returned if the object with the given ID is not found. In case of multiple document request, HTTP status 200 with an empty array is returned.
Updates one or more documents with changes specified as in MongoDB's $set
operator. In case of multiple documents, each document's update specification needs to be specified differently. This is not the same as MongoDB's update command, It is, instead, a bulk write operation.
Variants:
collection/_id
: Updates a single document using MongoDB$set
operator using the request body.collection
: The request body must be an array of objects, each with an_id
field. The rest of the object will be supplied to the$set
operator.
Success response:
{
result: {
ok: 1
},
matchedCount: 0,
modifiedCount: 0
}
Failure response (Missing IDs):
{
status: 'error',
statusCode: 400,
message: 'Missing _id in update (index 0)'
}
Replaces one more more documents with the given document(s). This is also a bulk write operation like the Update (PATCH) operation.
Variants:
collection/_id
: Replaces a single document identified by_id
with the one given in the request body.collection
: The request body is expected to be an array of objects, with each object containing an_id
field, which will identify the document and replace it with the given one. Non-existent documents will be created, as in an upsert operation.
Success response:
{
result: {
ok: 1,
nModified: 0,
nUpserted: 1
},
matchedCount: 0,
modifiedCount: 1
}
Failure response (Missing IDs):
{
status: 'error',
statusCode: 400,
message: 'Missing _id in update (index 0)'
}
Deletes one or more documents. To delete multiple documents, an optional filter selects the documents to be deleted.
Variants:
collection/_id
: Deletes a single document identified by_id
.collection
,[?<query>]
: Clears the entire collection or a filtered subset described by the filter in the query params. We use query-params-mongo for parsing the query string to get a filter. Or, the MongoDB filter can be specified directly in a parameter named__filter
.
Returns:
{
result: {
ok: 1
},
deletedCount: 1
}
The request body is parsed using mongodb-extjson, so data types that are native to MongoDB but not supported by the JSON format can also be specified. The Extended JSON documentation has a detailed specification of all the data types.
A few common ones are described here:
Type | Example | Resulting Value |
---|---|---|
ObjectId | { "id": { "$oid: "5ecce33370eef71be8ba4b5a" } | ObjectId("5ecce33370eef71be8ba4b5a") |
Long | { "n": { "$numberLong": "1584963168000" } | NumberLong(1584963168000) |
Date | { "t": { "$date": "2020-01-01T12:13:14.123Z" } | ISODate("2020-01-01T12:13:14.123Z") |
{ "d": { "$date": { "numberLong": "1593159811000" } } | ISODate("2020-06-26T08:23:31Z") |