jeve
v0.3.0
Published
Quick way to REST
Downloads
2
Maintainers
Readme
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
- Quick start
- Settings
- Schema
- Resource methods
- Item methods
- Validations
- Params & queries
- Middleware
- Custom routes
- Accessing models
- Self-written documentation
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.
| key | value | default | dotenv |
| -------- | ------ | --------------------- | -------- |
| port | number | 5000
| PORT |
| database | string | mongodb://localhost
| DATABASE |
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 | | ------ | ----------------------------------------- | | total | The total number of documents found | | limit | Max results per page | | page | The page which the cursor is currently on | | pages | The 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:
| method | function |
| ------ | --------------- |
| GET | jeve.get()
|
| POST | jeve.post()
|
| PUT | jeve.put()
|
| PATCH | jeve.patch()
|
| DELETE | jeve.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.