RESTHeart with FerretDB: a tutorial

Introduction

This tutorial introduces FerretDB, an open-source alternative to MongoDB built on PostgreSQL. Utilizing FerretDB in conjunction with RESTHeart, a powerful API server for MongoDB, this guide demonstrates how to set up the FerretDB stack using Docker and interact with it using RESTHeart's REST API.

The tutorial covers creating databases, populating collections, querying and updating documents, providing an efficient and compatible MongoDB-like experience while leveraging the benefits of PostgreSQL for storage and management.

RESTHeart's Main Features

The following picture illustrates RESTHeart's main features:

RESTHeart Features

Tutorial

The subsequent steps will guide you in experimenting with RESTHeart's REST API on top of FerretDB + PostgreSQL, as an alternative to a typical MongoDB instance. This is facilitated by RESTHeart's ability to automatically provide REST (and GraphQL) APIs on top of any MongoDB-compatible database.

1. Run the Necessary Services

The quickest way to initiate the software stack is by utilizing Docker Compose.

To launch the software stack and execute the tutorial from the command line, you will need:

  • httpie - A simple yet powerful command-line HTTP and API testing client for the API era.
  • Docker for your operating system.

Let's begin by creating the following docker-compose.yml file:

services:
  postgres:
    image: postgres
    environment:
      - POSTGRES_USER=username
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=ferretdb
    volumes:
      - ./data:/var/lib/postgresql/data

  ferretdb:
    image: ghcr.io/ferretdb/ferretdb
    restart: on-failure
    ports:
      - 27017:27017
    environment:
      - FERRETDB_POSTGRESQL_URL=postgres://postgres:5432/ferretdb

  restheart:
    image: softinstigate/restheart
    environment:
      RHO: >
        /mclient/connection-string->"mongodb://username:password@ferretdb/ferretdb?authMechanism=PLAIN"; /http-listener/host->"0.0.0.0";
    depends_on:
      - ferretdb
    ports:
      - "8080:8080"

networks:
  default:
    name: ferretdb

Navigate to the folder containing the docker-compose.yml file and start the services with:

$ docker-compose up -d

Then tail the logs with docker-compose logs -f restheart, and it will display something similar to the following output:

$ docker-compose logs -f restheart

08:43:26.866 [main] INFO  org.restheart.Bootstrapper - Starting RESTHeart instance default
08:43:26.868 [main] INFO  org.restheart.Bootstrapper - Version 7.5.1
...
08:43:28.567 [main] INFO  o.r.mongodb.MongoClientSingleton - Connecting to MongoDB...
08:43:28.835 [main] INFO  o.r.mongodb.MongoClientSingleton - MongoDB version 6.0.42
08:43:28.843 [main] WARN  o.r.mongodb.MongoClientSingleton - MongoDB is a standalone instance.
...
08:43:29.602 [main] INFO  org.restheart.Bootstrapper - RESTHeart started

The next steps assume that RESTHeart is running on localhost with the default configuration: the restheart database is bound to /, and the user "admin" exists with the default password "secret".

2. Create the Database

Verify that httpie is installed correctly:

$ http --version
3.2.2

Then use the PUT verb to bind the restheart database to /:

$ http -a "admin:secret" PUT :8080/
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Location, ETag, Auth-Token, Auth-Token-Valid-Until, Auth-Token-Location, X-Powered-By
Auth-Token: 5cepnnb9d7k45ob3erlmn183x450mexwes6i938bsnbw0bzzyd
Auth-Token-Location: /tokens/admin
Auth-Token-Valid-Until: 2023-10-05T09:23:17.535887832Z
Connection: keep-alive
Content-Length: 0
Content-Type: application/json
Date: Thu, 05 Oct 2023 09:08:17 GMT
ETag: 651e7d01fd19df010a6eb1e2
X-Powered-By: restheart.org

Note the complete list of HTTP headers returned by RESTHeart's response. The next examples will omit them for more clarity.

3. Create the inventory Collection

The examples in this tutorial utilize the inventory collection under the restheart database. To create this collection, run:

$ http -a "admin:secret" PUT :8080/inventory
HTTP/1.1 201 Created
...

4. Insert Multiple Documents to Populate the Collection

To populate the inventory collection, create an inventory.json file with the following content (a JSON array of documents), or just download it from here.

[
   { "item": "journal", "qty": 25, "size": { "h": 14, "w": 21, "uom": "cm" }, "status": "A" },
   { "item": "notebook", "qty": 50, "size": { "h": 8.5, "w": 11, "uom": "in" }, "status": "A" },
   { "item": "paper", "qty": 100, "size": { "h": 8.5, "w": 11, "uom": "in" }, "status": "D" },
   { "item": "planner", "qty": 75, "size": { "h": 22.85, "w": 30, "uom": "cm" }, "status": "D" },
   { "item": "postcard", "qty": 45, "size": { "h": 10, "w": 15.25, "uom": "cm" }, "status": "A" }
]

Then POST the file to RESTHeart:

$ cat inventory.json | http -a "admin:secret" POST :8080/inventory
HTTP/1.1 200 OK
...

{
    "deleted": 0,
    "inserted": 5,
    "links": [
        "/inventory/651e8084fd19df010a6eb1e8",
        "/inventory/651e8084fd19df010a6eb1e9",
        "/inventory/651e8084fd19df010a6eb1ea",
        "/inventory/651e8084fd19df010a6eb1eb",
        "/inventory/651e8084fd19df010a6eb1ec"
    ],
    "matched": 0,
    "modified": 0
}

5. GET All Documents

Let’s retrieve all documents in a row. For this, we send a GET request to the entire collection:

$ http -a "admin:secret" GET :8080/inventory
HTTP/1.1 200 OK
...

[
    {
        "_etag": {
            "$oid": "651e8084fd19df010a6eb1e7"
        },
        "_id": {
            "$oid": "651e8084fd19df010a6eb1ec"
        },
        "item": "postcard",
        "qty": 45,
        "size": {
            "h": 10,
            "uom": "cm",
            "w": 15.25
        },
        "status": "A"
    },
    {
        "_etag": {
            "$oid": "651e8084fd19df010a6eb1e7"
        },
        "_id": {
            "$oid": "651e8084fd19df010a6eb1eb"
        },
        "item": "planner",
        "qty": 75,
        "size": {
            "h": 22.85,
            "uom": "cm",
            "w": 30
        },
        "status": "D"
    },
    {
        "_etag": {
            "$oid": "651e8084fd19df010a6eb1e7"
        },
        "_id": {
            "$oid": "651e8084fd19df010a6eb1ea"
        },
        "item": "paper",
        "qty": 100,
        "size": {
            "h": 8.5,
            "uom": "in",
            "w": 11
        },
        "status": "D"
    },
    {
        "_etag": {
            "$oid": "651e8084fd19df010a6eb1e7"
        },
        "_id": {
            "$oid": "651e8084fd19df010a6eb1e9"
        },
        "item": "notebook",
        "qty": 50,
        "size": {
            "h": 8.5,
            "uom": "in",
            "w": 11
        },
        "status": "A"
    },
    {
        "_etag": {
            "$oid": "651e8084fd19df010a6eb1e7"
        },
        "_id": {
            "$oid": "651e8084fd19df010a6eb1e8"
        },
        "item": "journal",
        "qty": 25,
        "size": {
            "h": 14,
            "uom": "cm",
            "w": 21
        },
        "status": "A"
    }
]

Note: RESTHeart's API supports automatic pagination of long JSON responses using the parameters page and pagesize. For example, to get the first page made of 10 items:

$ http -a "admin:secret" GET :8080/inventory\?page\=1\&pagesize\=10

6. Query Documents with Filters

It’s possible to apply a filter at the end of the request to query for specific documents. The following request asks for all documents with a "qty" property greater than 75, using the MongoDB's query syntax.

$ http -a "admin:secret" GET :8080/inventory\?filter\='{"qty":{"$gt":75}}'
HTTP/1.1 200 OK
...

[
    {
        "_etag": {
            "$oid": "651e8084fd19df010a6eb1e7"
        },
        "_id": {
            "$oid": "651e8084fd19df010a6eb1ea"
        },
        "item": "paper",
        "qty": 100,
        "size": {
            "h": 8.5,
            "uom": "in",
            "w": 11
        },
        "status": "D"
    }
]

7. Insert an additional document

Now we are going to add a new document to the inventory collection using the POST verb:

$ echo '{"item": "newItem", "qty": 10, "size": { "h": 2, "w": 4, "uom": "cm" }, "status": "C"}' \
| http -a "admin:secret" POST :8080/inventory
HTTP/1.1 201 Created
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Location, ETag, Auth-Token, Auth-Token-Valid-Until, Auth-Token-Location, X-Powered-By
Auth-Token: 5cepnnb9d7k45ob3erlmn183x450mexwes6i938bsnbw0bzzyd
Auth-Token-Location: /tokens/admin
Auth-Token-Valid-Until: 2023-10-05T10:03:38.327437695Z
Connection: keep-alive
Content-Length: 0
Content-Type: application/json
Date: Thu, 05 Oct 2023 09:48:38 GMT
ETag: 651e8676fd19df010a6eb1f2
Location: http://localhost:8080/inventory/651e8676fd19df010a6eb1f3
X-Powered-By: restheart.org

Note the Location header in the response contains a link to the newly created document. The above string 651e8676fd19df010a6eb1f3 is the actual unique ID of the newly inserted document (in your case it will be different, of course) automatically created by RESTHeart.

To get the document you can directly copy that link and use it in a subsequent query. For example:

$ http -a "admin:secret" GET http://localhost:8080/inventory/651e8676fd19df010a6eb1f3

8. PUT a new document

POST will create a new document using the ID automatically provided by the database. However, it’s possible to instead PUT a document into the collection by explicitly specifying the document ID at the end of the request. iIn this case we need to add the query parameter ?wm=upsert since PUT default write mode is update:

$ echo '{ "item": "yetAnotherItem", "qty": 90, "size": { "h": 3, "w": 4, "uom": "cm" }, "status": "C" } \n' \
| http -a "admin:secret" PUT :8080/inventory/newDocument\?wm\=upsert
HTTP/1.1 201 Created
...

You can get this specific document using the newDocument ID, like this:

$ http -a "admin:secret" GET :8080/inventory/newDocument

9. Update a document

To update the document identified by the newDocument ID in the collection we are using the PATCH verb.

$ echo '{ "qty": 40, "status": "A", "newProperty": "value" }' \
| http -a "admin:secret" PATCH :8080/inventory/newDocument
HTTP/1.1 200 OK
...

To check the modifications:

$ http -a "admin:secret" GET :8080/inventory/newDocument
HTTP/1.1 200 OK
...

{
    "_etag": {
        "$oid": "651e8cd0fd19df010a6eb1f9"
    },
    "_id": "newDocument",
    "item": "yetAnotherItem",
    "newProperty": "value",
    "qty": 40,
    "size": {
        "h": 3,
        "uom": "cm",
        "w": 4
    },
    "status": "A"
}

The last request changes the document created in the previous example as indicated in the request body.

10. Delete a document

To delete a document we use the DELETE verb:

$ http -a "admin:secret" DELETE :8080/inventory/newDocument
HTTP/1.1 204 No Content
...

If we try to GET the deleted document, RESTHeart returns an HTTP 404 "Not Found" error:

$ http -a "admin:secret" GET :8080/inventory/newDocument
HTTP/1.1 404 Not Found
...

{
    "http status code": 404,
    "http status description": "Not Found",
    "message": "document 'newDocument' does not exist"
}

11. Get the size of a collection

To count the number of documents in a collection, use the _size resource:

$ http -a "admin:secret" GET :8080/inventory/_size

It returns a JSON document like this:

HTTP/1.1 200 OK
...

{
    "_size": 6
}

Conclusion

In this tutorial, we've explored how to set up and use FerretDB + PostgreSQL as a MongoDB-compatible database, and we've demonstrated how to interact with it using RESTHeart's REST API. This combination provides a versatile and robust alternative to traditional MongoDB setups, offering the flexibility and familiarity of MongoDB while leveraging the power of PostgreSQL for storage and management.

References

  • RESTHeart - REST, GraphQL and WebSocket API server for MongoDB and any compatible database.
  • FerretDB - A truly Open Source MongoDB alternative, built on Postgres