0.3.0 • Published 12 months ago

jeve v0.3.0

Weekly downloads
-
License
MIT
Repository
github
Last release
12 months ago

Jeve is a JavaScript framework for effortlessly building an API with a self-written documentation. Powered by Express and Mongoose for native MongoDB support. Inspired by PyEve for Python.

Table of contents

Install

$ npm install jeve

Quick start

Jeve runs on port 5000 and uses mongodb://localhost as the default MongoDB connection string URI. Presuming that the database is running locally on the default port the following code is the bare minimum to get the API up and running:

const Jeve = require('jeve')

const settings = {
    domain: {
        people: {},
    },
}

const jeve = new Jeve(settings)
jeve.run()

That's all it takes for the API to go live with an endpoint. We can now try to GET /people.

$ curl -i http://localhost:5000/people
HTTP/1.1 204 No Content

We're live but the endpoint returns no content since we haven't set up a schema. More about that later.

Settings

In order to change the default port and MongoDB URI simply add the keys to the root of the settings object. They can also be added in a .env file. In case values exists both in .env and in the root of the settings object, the root value will be superseded by dotenv.

keyvaluedefaultdotenv
portnumber5000PORT
databasestringmongodb://localhostDATABASE

If we would want to run Jeve on port 5100 instead for example:

const settings = {
    domain: {
        people: {},
    },
    port: 5100,
}

Schema

Jeve will automatically create Mongoose Models based on a schema object under each domain. Let's add name and age to the people domain as types string and number.

const settings = {
    domain: {
        people: {
            schema: {
                name: 'string',
                age: 'number',
            },
        },
    },
}

Types can be written directly as a string:

name: 'string'

Or as an object with the type key:

name: {
  type: 'string',
}

The latter is needed if other validations like required or unique are going to be added. In our example, name is a required field.

name: {
  type: 'string',
  required: true,
}

The following types are supported:

  • string
  • number
  • date
  • boolean
  • objectid
  • object
  • array

We now have a settings object which looks like this:

const settings = {
    domain: {
        people: {
            schema: {
                name: {
                    type: 'string',
                    required: true,
                },
                age: 'number',
            },
        },
    },
}

Resource methods

Our endpoint /people allows HTTP method GET by default. In order to save a person to the database, we need to add the POST method to our domain resource.

Resource methods are added as strings in an array on the same root as our schema object. Valid HTTP methods are GET and POST.

Before adding this to our settings, let's try to POST.

$ curl -i -X POST http://localhost:5000/people
HTTP/1.1 404 Not Found

As expected, we get a 404 status code response. Add the following to the people object and try again:

people: {
  resourceMethods: ['GET', 'POST'],
  schema: { ... },
}
$ curl -i -X POST http://localhost:5000/people
HTTP/1.1 400 Bad Request

This time we get a 400 status code response instead along with a json error message since we sent an empty body request.

{
  "_success": false,
  "_issues": [
    {
      "name": "required field"
    }
  ]
}

If we instead send a proper POST application/json with a name:

curl -i -d '{"name":"James Smith"}' -H "Content-Type: application/json" -X POST http://localhost:5000/people
HTTP/1.1 201 Created

Now our first successful POST was made!

{
  "_success": true,
  "_item": {
    "_id": "62eebccf0c5aa6efc2d8ceed",
    "name": "James Smith",
    "_created": "2022-08-06T19:11:11.627Z",
    "_updated": "2022-08-06T19:11:11.627Z"
  }
}

By default only GET methods are allowed unless an array of resourceMethods have been defined. If however, you'd like an endpoint only serving POST requests, simply add that as the single value to the array.

Item methods

In the previous example our request returned an item with an _id. If we wanted to access only this item in a GET request, we could add the _id as a parameter to the request: /people/62eebccf0c5aa6efc2d8ceed.

By default the only valid HTTP method is GET, however if we would want other methods to be allowed we simply add them to our itemMethods array in a similar way as the resourceMethods.

The main difference to think about is that resource methods take care of the domain itself, accessing /people in order to GET a list of documents or POST a new document. While item methods handle a mandatory parameter which is the _id of the document in order to GET that specific document or handle updates or deletions. Valid methods are:

  • GET
  • PUT
  • PATCH
  • DELETE

Validations

Schema keys are actual field names and in case the value is an object instead of the type as a string the following validation rules are can be used:

An example where name has to contain at least 2 characters and email must be unique within the collection:

{ /* ... */
  name: {
    type: 'string',
    required: true,
    minLength: 2
  },
  email: {
    type: 'string',
    required: true,
    unique: true,
  }
}

Params & queries

If we make a new GET request to /people, we now get everything that's stored in the database.

$ curl -i http://localhost:5000/people
HTTP/1.1 200 OK
{
  "_success": true,
  "_items": [
    {
      "_id": "62eebccf0c5aa6efc2d8ceed",
      "name": "James Smith",
      "_created": "2022-08-06T19:11:11.627Z",
      "_updated": "2022-08-06T19:11:11.627Z"
    }
  ],
  "_meta": {
    "total": 1,
    "limit": 10,
    "page": 1,
    "pages": 1
  }
}

In our case the _items array only contains one (1) object, the one we just added. Responses are paginated by default and the _meta object contains information about the specific endpoint.

_meta description
totalThe total number of documents found
limitMax results per page
pageThe page which the cursor is currently on
pagesThe total number of pages

If we imagined that we had 12 documents in the /people collection the _meta response would look something like this:

{
  "_success": true,
  "_items": [ /* ... */ ],
  "_meta": {
    "total": 12,
    "limit": 10,
    "page": 1,
    "pages": 2
  }
}

Since we know there's a second page, we can simply do a new GET with the page query:

$ curl -i "http://localhost:5000/people?page=2"
HTTP/1.1 200 OK

If we wanted more results per page:

$ curl -i "http://localhost:5000/people?limit=20"
HTTP/1.1 200 OK

If we're looking for a specific document the _id of that document needs to follow as a parameter, for example /people/62eebccf0c5aa6efc2d8ceed. This is the way PATCH, PUT and DELETE knows what document to handle as well.

Other valid queries are sort, where and select.

If we wanted our result to be sorted by their creation date we could send the /people?sort=_created query as an example. Or if we wanted to reverse the search, simply add - before the key value: /people?sort=-_created.

The last two parameters accepts json-input, where will filter the request. If for example we only wanted a list of people older than 18 we could use the following query: /people?where={"age":{"$gte": 18}}. select will filter the documents, as in including specific fields or excluding others. If we for example weren't interested in the ages, we could exclude that field by specifying the key along with a 0: /people?select={"age":0}.

Middleware

Each domain accepts a preHandler function which will run before the request. Use cases range from authorization to catching data and manipulating the body. As an example, let's imagine we had a boolean value for the field isAdult in our schema. We're not sending this value in our request, but we want our middleware to catch it.

{ /* ... */
  people: {
    resourceMethods: ['GET', 'POST'],
    schema: {
      age: 'number',
      isAdult: 'boolean',
    },
    preHandler: checkIfAdult,
  }
}

In our middleware function checkIfAdult, we would simply add the value to it. Don't forget to call with next()...

function checkIfAdult(req, res, next) {
    const age = req.body?.age
    if (age) req.body.isAdult = age >= 18
    next()
}

Custom routes

Jeve supports custom routes:

methodfunction
GETjeve.get()
POSTjeve.post()
PUTjeve.put()
PATCHjeve.patch()
DELETEjeve.delete()

A simple example of a /greeting route that returns the text Hello World!:

jeve.get('/greeting', (req, res) => {
    res.send('Hello World!')
})

If greeting exists in our domain object, the custom route will be skipped and not initialized due to conflict and a message will be shown in the console. However, if the path is deeper, for example /greeting/swedish, the custom route will be created.

Accessing Models

Every model that's dynamically created by Jeve is accessible from the jeve.model() function. If we for example wanted to access a model from a custom route and use any native Mongoose function with it:

jeve.get('/greeting/:id', async (req, res) => {
    const { id } = req.params
    const person = await jeve.model('people').findOne({ _id: id })
    res.send(`Hello ${person.name}`)
})

Self-written documentation

Jeve will dynamically create it's own documentation and the UI is accessible at /docs in the browser. The documentation contains all available routes in the settings object and will show which resource and item methods they're accessible by. The accordion contains the schema object, an overview of keys and validations.

npm.io

0.3.0

12 months ago

0.1.0

2 years ago

0.2.1

2 years ago

0.1.2

2 years ago

0.2.0

2 years ago

0.1.1

2 years ago

0.1.4

2 years ago

0.2.2

2 years ago

0.1.3

2 years ago

0.1.6

2 years ago

0.1.5

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago