jsonapi-server-mini
Super easy Node.js + MongoDB CRUD backend for JSON:API consuming apps like Ember.
jsonapi-server-mini
allows you to quickly write an ExpressJS JSON:API server. Adhering to Convention Over Configuration, your endpoints should have zero boilerplate code, and only contain your business logic.
Scope
This module attempts to be a simplistic lightweight yet complete JSON:API endpoint. There should be out-of-the-box support for all basic JSON:API features you need to write a decent app, like sort
, filter
, page
and include
. Routes are limited to one type, one model, one schema. If you want something crazy, like a custom type that uses six models and a hard-coded joke about Belgians, you should look for a more advanced solution, such as the ones listed below:
- Fortune.js with fortune-json-api
- jsonapi-server (no longer maintained)
- express-autoroute with express-autoroute-json
This project was forked from express-autoroute-json
because its wide scope made it hard to land pull requests. jsonapi-server-mini
is KISS, so we can implement the full JSON:API base spec without taking complexities into account.
Quick-start
npm install --save jsonapi-server-mini
Super easy quick start
The module contains everything to get up and running. Even test routes /tests
and /users
. Don't worry, they will go away once you've defined your own models. But for now, check this out!
Note: For this super easy quick start, an authless mongo database is expected to run at port
27017
. You can run one for testing purposes usingdocker
:docker run -d -p 27017:27017 --rm --name jsonapi-server-mini-mongo mongo
mkdir jsonapi-server-mini && cd $_npm init -ynpm install --save jsonapi-server-miniecho "require('jsonapi-server-mini')()" > index.jsnode .
Bam! We're running a server. Let's create a user
curl -X POST http://localhost:8888/api/users \ -H 'Content-Type: application/vnd.api+json' \ -d @- <<'EOF'{ "data": { "type": "users", "attributes": { "name": "Some User", "email": "test@example.com" } }}EOF
OMG! Is this real? Can we fetch the user?
wget http://localhost:8888/api/users | jq
Response:
"data": "type": "users" "id": "5c1d81d48a162b5776ab7fd0" "attributes": "name": "Some User" "email": "test@example.com"
Route definitions
The above /api/users
example route is active because you haven't defined your own resources. The fallback from node_modules/jsonapi-server-mini/routes/api/user.js
is loaded:
module schema : name : String email : String // CRUD operations. Remove to disable. find : {} create : {} update : {} delete : {}
Our first app
You should create your own routes in your app's routes
directory. You'll need to specify the full path to your own routes
directory. Now let's create our first fully working app:
index.js
:
const path = const jsMini =
Any sub-directory will be appended to the route in case you want /api/v1
, /api/v2
etc. The filename (in singular form) decides the resource type and schema name (in plural form). Go ahead and copy the file, adding something crazy to the schema!
Module options
When starting jsonapi-server-mini, we can provide an options
Object like so: jsMini(options)
. Every option is, well, optional. But it makes sense to at least define your own routes, and specify the path to them using routes
.
app
Router Express routerauthn
function Basic first-line authenticationlimit
Number Global limit forfind
queries. Default:50
limitMax
Number Maximum query string limit override. Default:100
logger
Module Default:winston
console loggermeta
Object Static metadata to append to every JSON:API response. E.g. server version information.mongoose
Module in case you want to re-use an existing instance.mongoUri
String MongoDB connection stringroutes
String Path to custom routes
app
If you want to use your own Express Router so you can add different (non-JSON:API) endpoints to your app, you can do so, but you'll need to enable some basic parsing for jsonapi-server-mini
to work on requests:
- Handle CORS
- Decode
urlencoded
requests - Parse body for
application/vnd.api+json
mimetype
Here is an example:
index.js
:
const path = const express = const bodyParser= const morgan = const cors = const jsMini = const app = const routes = path app // jsonapi-server-mini endpoint // custom endpoint
authn
authn
function(req, res, next) -> Callback
mongoUri
If you are not running an authless MongoDB server on localhost, you need to provide a MongoDB connection string. E.g.:
Route options
schema
Schema A Mongoose Schemadescription
String Describe your routeindexes
Object or Array Define one or more indexes
Indexes
You can define a single compound index that cannot be defined in the schema itself.
module schema : name : String group : type: String required: true friends : type: String required: true indexes : group : 1 friends : 1 // CRUD operations. Remove to disable. find : {}
Or multiple compound indexes.
indexes : group : 1 friends : 1 group : 1 name : 1
You can specify index options only when writing your indexes in an array, like the second example above. Then wrap your index in a second array, and specify the options as the second item.
indexes : createdAt : 1 expireAfterSeconds : 3600 // [ another index ]
Note: Although this is useful for development, it's recommended to define indexes manually, so that your application restarts faster.
CRUD method options
Schema
For defining your schema, take a look at the Mongoose Schema documentation. Try not to make it too complex; it needs to map to JSON:API. Basically, everything is an attribute
. To define a relationship
, first create a route for the type of the relationship, e.g. test.js
. Next, specify a property with its type
set to ObjectID
and its ref
to the (capitalized) (file)name of the new resource you just created:
module schema : name : String email : String // One-to-many relationship myTests : type : mongooseSchemaTypesObjectId ref : 'Test' // One-to-one relationship lastUsed : type : mongooseSchemaTypesObjectId ref : 'Test' // CRUD operations. Remove to disable. find : {}
CRUD operations
The comment says it all. You can remove them to disable them, or add route-specific options.
Authentication
Once you've written some custom authentication logic, you can create an Express Middleware function authn
to either continue or throw an error. The same authentication applies to all CRUD methods.
module schema { // Allow access for user if requser return // Deny access for everyone else } // CRUD operations. Remove to disable. find : {}
Let's make it a bit more complex. What if you want to be publicly readable, but only writable for a user, and only deletable by an admin? Simply move the method-specific auth
function to the method object:
{ // Allow access for user if reslocalsuser return // Deny access for everyone else } module schema // CRUD operations. Remove to disable. find : {} create : authn update : authn delete : { // Allow access for admin if reslocalsuser && reslocalsisAdmin return // Deny access for everyone else }
You may have a global authn
function in your jsMini
defenition, but are looking to pass an otherwise rejected authentication. To do this, you can use an upstream authn
function to set a variable that you will check downstream.
someRoute.js
:
module description : 'This route will override global authn' schema { reslocalsauthenticated = true return } find
index.js
:
const jsMini = { if reslocalsauthenticated === true return // Granted elsewhere}
Authorization
Even when your user is authenticated, they are probably not supposed to access data from another user. Authorization allows for more granular control. Add the authz
function to the specific CRUD, route, or global configuration you would like to apply rules to.
You can either add full express middleware:
{ const query = reqjsMini const isAdmin = reslocalsrole == 'admin' const adminOnly = $ne: true if isAdmin reqjsMiniquery = ...query adminOnly }
A shortcut function with one argument, returning a mandatory query selector:
{ const userId = reqreslocals return userId }
Or a shortcut function simply returning a boolean indicating access granted or denied:
{ return 'subscriber' 'editor'}
Example
{ // Allow access for user if reslocalsuser return // Deny access for everyone else } { const userId = reqreslocals // Assuming user created resources have the users' userId attribute return userId } module schema // Everyone can find find : {} // All users can create create : authn // Only owners can update or delete update : authn authz delete : authn authz
JSON:API support
The current implementation attempts to follow the basics of the JSON:API 1.0 spec.
Sort
Allow sorting using a preconfigured sort
Object on the route configuration, or a sort
String in your query.
?sort=-date
Filter
Filter (or 'query') results with a filter
String in your query.
?filter[type]=order&filter[quantity]=<10
There are a couple of filter operators for making advanced queries:
<=
like MongoDB's$lte
>=
like MongoDB's$gte
<
like MongoDB's$lt
>
like MongoDB's$gt
:
partial match (case insensitive)~
exact match (case insensitive):~
ends with (case insensitive)~:
starts with (case insensitive)!
is like MongoDB's$ne
Fields
Choose what fields to select from documents. You can either predefine this in your route, or instruct the jsonapi server with your querystring.
Fields preconfigured in route
Add a field
object to your route, with keys being the fieldnames you want to include (1
) or exclude (0
). You can only include or exclude all fields. Not a mix of both.
module // ... fields : bio : 0 friends : 0
Fields instructed in querystring
Submit a comma separated value with the name of fields you want to include. or a list of names you want to exclude prepended by a minus symbol.
GET api/users?fields=-bio,-friends
Include
At the moment, includes work one level deep.
Advanced
Middleware
Perhaps you're trying to do something slightly more complicated, but you are not ready for Fortune.js yet. You might benefit from using a custom middleware function right before your query is executed. You can specify this function on multiple levels. The CRUD level, the route level, and the global level (when initializing jsMini
).
module // ... { // Executed before _every_ crud operation on this route return } find : { // Executed before every _find_ operation on this route return
Shortcut middleware
Often you'll just want to pre-fill the query (i.e. req.jsMini.query
) with a non-async object-returning function, similar to authz
. You can suffice with a single req
parameter:
{ return customFilter: 'presetValue' }
Other middleware
You can turn middleware into an object with multiple hooks. pre(req)
is the same as middleware(req)
. With this notation, you can add advanced middleware:
middleware { /* ... */ } { /* ... */ } // findOne or findMany { /* ... */ }
Note that beforeSerializer
and afterSerializer
will be applied to both GET
and PATCH
requests. We can't have carefully hidden attributes show up by sending a PATCH
request.
Contributing
We like to keep the scope of this module very small and easy to maintain in order to allow for quickly building small yet decent apps. Any pull request that adds complexity not part of the JSON:API spec will be rejected.
Running tests
Stop anything running on port 27017
, and start an authless mongo database using docker
:
npm run start-docker
Install dependencies:
npm install
Run mocha:
npm run test-mocha
License
Copyright (c) 2018 - 2019, Redsandro Media info@redsandro.com
Copyright (c) 2014 - 2018, Stone Circle info@stonecircle.ie
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.