LoopBack is a mobile backend framework that you can run in the cloud or on-premises. It is built on StrongNode and open-source Node.js modules. For more information on the advantages of using LoopBack, see StrongLoop | LoopBack.
To gain a basic understanding of key LoopBack concepts, read the following Overview section. Then, dive right into creating an app in Quick Start.
LoopBack consists of:
slc lb
, for creating and working with LoopBack applications.As illustrated in the diagram below, a LoopBack application has three components:
An app interacts with data sources through the LoopBack model API, available locally within Node.js, remotely over REST, and via native client APIs for iOS, Android, and HTML5. Using the API, apps can query databases, store data, upload files, send emails, create push notifications, register users, and perform other actions provided by data sources.
Mobile clients can call LoopBack server APIs directly using Strong Remoting, a pluggable transport layer that enables you to provide backend APIs over REST, WebSockets, and other transports.
##Quick Start
This section will get you up and running with LoopBack and the StrongLoop sample app in just a few minutes.
You must have the git
command-line tool installed to run the sample application.
If needed, download it at http://git-scm.com/downloads and install it.
On Linux systems, you must have root privileges to write to /usr/share
.
NOTE: If you're using Windows or OSX and don't have a C compiler (Visual C++ on Windows or XCode on OSX) and command-line "make" tools installed, you will see errors such as these:
xcode-select: Error: No Xcode is selected. Use xcode-select -switch <path-to-xcode>,
or see the xcode-select manpage (man xcode-select) for further information.
...
Unable to load native module uvmon; some features may be unavailable without compiling it.
memwatch must be installed to use the instances feature
StrongOps not configured to monitor. Please refer to http://docs.strongloop.com/strong-agent for usage.
You will still be able to run the sample app, but StrongOps will not be able to collect certain statistics.
Follow these steps to run the LoopBack sample app:
If you have not already done so, download and install StrongLoop Suite or set up your cloud development platform.
Setup the StrongLoop Suite sample app with this command.
$ slc example
This command clones the sample app into a new directory
named sls-sample-app
and installs all of its dependencies.
Run the sample application by entering this command:
$ cd sls-sample-app
$ slc run app
To see the app running in a browser, open http://localhost:3000. The app homepage lists sample requests you can make against the LoopBack REST API. Click the GET buttons to see the JSON data returned.
The StrongLoop sample is a mobile app for "Blackpool," an imaginary military equipment rental dealer with outlets in major cities around the world. It enables customers (military commanders) to rent weapons and buy ammunition from Blackpool using their mobile phones. The app displays a map of nearby rental locations and see currently available weapons, which you can filter by price, ammo type and distance. Then, you can use the app to reserve the desired weapons and ammo.
Note that the sample app is the backend functionality only; that is, the app has a REST API, but no client app or UI to consume the interface.
For more details on the sample app, see StrongLoop sls-sample-app in GitHub.
Follow these steps to explore the sample app's REST API:
/locations/{id}
:You can see the request URL, the JSON in the response body, and the HTTP response code and headers.
To gain a deeper understanding of LoopBack and how it works, read the following sections, Working with Models and Working with Data Sources and Connectors.
For information on how StrongLoop Suite provides:
A LoopBack model consists of:
Apps use the model API to display information to the user or trigger actions on the models to interact with backend systems. LoopBack supports both "dynamic" schema-less models and "static", schema-driven models.
Dynamic models require only a name. The format of the data are specified completely and flexibly by the client application. Well-suited for data that originates on the client, dynamic models enable you to persist data both between accessTokens and between devices without involving a schema.
Static models require more code up front, with the format of the data specified completely in JSON. Well-suited to both existing data and large, intricate datasets, static models provide structure and consistency to their data, preventing bugs that can result from unexpected data in the database.
Here is a simple example of creating and using a model.
Consider an e-commerce app with Product
and Inventory
models.
A mobile client could use the Product
model API to search through all of the
products in a database. A client could join the Product
and Inventory
data to
determine what products are in stock, or the Product
model could provide a
server-side function (or remote method) that aggregates this
information.
For example, the following code creates product and inventory models:
var Model = require('loopback').Model;
var Product = Model.extend('product');
var Inventory = Model.extend('customer');
The above code creates two dynamic models, appropriate when data is "free form." However, some data sources, such as relational databases, require schemas. Additionally, schemas are valuable to enable data exchange and to validate or sanitize data from clients; see Sanitizing and Validating Models.
A data source enables a model to access and modify data in backend system such as a relational database.
Attaching a model to a data source, enables the model to use the data source API. For example, as shown below, the MongoDB Connector, mixes in a create
method that you can use to store a new product in the database; for example:
// Attach data sources
var db = loopback.createDataSource({
connector: require('loopback-connector-mongodb')
});
// Enable the model to use the MongoDB API
Product.attachTo(db);
// Create a new product in the database
Product.create({ name: 'widget', price: 99.99 }, function(err, widget) {
console.log(widget.id); // The product's id, added by MongoDB
});
Now the models have both data and behaviors. Next, you need to make the models available to mobile clients.
To expose a model to mobile clients, use one of LoopBack's remoting middleware modules.
This example uses the app.rest
middleware to expose the Product
Model's API over REST.
For more information on LoopBack's REST API, see REST API.
// Step 3: Create a LoopBack application
var app = loopback();
// Use the REST remoting middleware
app.use(loopback.rest());
// Expose the `Product` model
app.model(Product);
After this, you'll have the Product
model with create, read, update, and delete (CRUD) functions working remotely
from mobile clients. At this point, the model is schema-less and the data are not checked.
A schema provides a description of a model written in LoopBack Definition Language, a specific form of JSON. Once a schema is defined for a model, the model validates and sanitizes data before passing it on to a data store such as a database. A model with a schema is referred to as a static model.
For example, the following code defines a schema and assigns it to the product model. The schema defines two fields (columns): name, a string, and price, a number. The field name is a required value.
var productSchema = {
"name": { "type": "string", "required": true },
"price": "number"
};
var Product = Model.extend('product', productSchema);
A schema imposes restrictions on the model: If a remote client tries to save a product with extra properties
(for example, description
), those properties are removed before the app saves the data in the model.
Also, since name
is a required value, the model will only be saved if the product contains a value for the name
property.
Data sources encapsulate business logic to exchange data between models and various back-end systems such as relational databases, REST APIs, SOAP web services, storage services, and so on. Data sources generally provide create, retrieve, update, and delete (CRUD) functions.
Models access data sources through connectors that are extensible and customizable.
Connectors implement the data exchange logic using database drivers or other
client APIs. In general, application code does not use a connector directly.
Rather, the DataSource
class provides an API to configure the underlying connector.
LoopBack provides several connectors, with more under development.
Connector | GitHub Module |
---|---|
Memory | Built-in to loopback-datasource-juggler |
MongoDB | loopback-connector-mongodb |
Oracle | loopback-connector-oracle |
MySQL | loopback-connector-mysql |
REST | loopback-connector-rest |
For more information, see the LoopBack DataSource and Connector Guide.
The Loopback library is unopinioned in the way you define your app's data and logic. Loopback also bundles useful pre-built models for common use cases.
Defining a model with loopback.createModel()
is really just extending the base loopback.Model
type using loopback.Model.extend()
. The bundled models extend from the base loopback.Model
allowing you to extend them arbitrarily.
Register and authenticate users of your app locally or against 3rd party services.
Extend a vanilla Loopback model using the built in User model.
// create a data source
var memory = loopback.memory();
// define a User model
var User = loopback.User.extend('user');
// attach to the memory connector
User.attachTo(memory);
// also attach the accessToken model to a data source
User.accessToken.attachTo(memory);
// expose over the app's api
app.model(User);
Note: By default the loopback.User
model uses the loopback.AccessToken
model to persist access tokens. You can change this by setting the accessToken
property.
Note: You must attach both the User
and User.accessToken
model's to a data source!
Create a user like any other model.
// username and password are not required
User.create({email: 'foo@bar.com', password: 'bar'}, function(err, user) {
console.log(user);
});
Create an accessToken
for a user using the local auth strategy.
Node.js
User.login({username: 'foo', password: 'bar'}, function(err, accessToken) {
console.log(accessToken);
});
REST
You must provide a username and password over rest. To ensure these values are encrypted, include these as part of the body and make sure you are serving your app over https (through a proxy or using the https node server).
POST
/users/login
...
{
"email": "foo@bar.com",
"password": "bar"
}
...
200 OK
{
"sid": "1234abcdefg",
"uid": "123"
}
Node.js
// login a user and logout
User.login({"email": "foo@bar.com", "password": "bar"}, function(err, accessToken) {
User.logout(accessToken.id, function(err) {
// user logged out
});
});
// logout a user (server side only)
User.findOne({email: 'foo@bar.com'}, function(err, user) {
user.logout();
});
REST
POST /users/logout
...
{
"sid": "<accessToken id from user login>"
}
Require a user to verify their email address before being able to login. This will send an email to the user containing a link to verify their address. Once the user follows the link they will be redirected to /
and be able to login normally.
// first setup the mail datasource (see #mail-model for more info)
var mail = loopback.createDataSource({
connector: loopback.Mail,
transports: [{
type: 'smtp',
host: 'smtp.gmail.com',
secureConnection: true,
port: 465,
auth: {
user: 'you@gmail.com',
pass: 'your-password'
}
}]
});
User.email.attachTo(mail);
User.requireEmailVerfication = true;
User.afterRemote('create', function(ctx, user, next) {
var options = {
type: 'email',
to: user.email,
from: 'noreply@myapp.com',
subject: 'Thanks for Registering at FooBar',
text: 'Please verify your email address!'
template: 'verify.ejs',
redirect: '/'
};
user.verify(options, next);
});
Send an email to the user's supplied email address containing a link to reset their password.
User.reset(email, function(err) {
console.log('email sent');
});
The password reset email will send users to a page rendered by loopback with fields required to reset the user's password. You may customize this template by defining a resetTemplate
setting.
User.settings.resetTemplate = 'reset.ejs';
Confirm the password reset.
User.confirmReset(token, function(err) {
console.log(err || 'your password was reset');
});
Identify users by creating accessTokens when they connect to your loopback app. By default the loopback.User
model uses the loopback.AccessToken
model to persist accessTokens. You can change this by setting the accessToken
property.
// define a custom accessToken model
var MyAccessToken = loopback.AccessToken.extend('MyAccessToken');
// define a custom User model
var User = loopback.User.extend('user');
// use the custom accessToken model
User.accessToken = MyAccessToken;
// attach both AccessToken and User to a data source
User.attachTo(loopback.memory());
MyAccessToken.attachTo(loopback.memory());
Send emails from your loopback app.
// extend a one-off model for sending email
var MyEmail = loopback.Email.extend('my-email');
// create a mail data source
var mail = loopback.createDataSource({
connector: loopback.Mail,
transports: [{
type: 'smtp',
host: 'smtp.gmail.com',
secureConnection: true,
port: 465,
auth: {
user: 'you@gmail.com',
pass: 'your-password'
}
}]
});
// attach the model
MyEmail.attachTo(mail);
// send an email
MyEmail.send({
to: 'foo@bar.com',
from: 'you@gmail.com',
subject: 'my subject',
text: 'my text',
html: 'my <em>html</em>'
}, function(err, mail) {
console.log('email sent!');
});
NOTE: the mail connector uses nodemailer. See the nodemailer docs for more info.
StrongLoop Suite includes a command-line tool, slc
(StrongLoop Command), for working with applications.
The slc lb
command enables you to quickly create new LoopBack applications and models with the following sub-commands:
For more information on the slc
command, see StrongLoop Control.
slc lb workspace wsname
Creates an empty directory named wsname. The argument is optional; default is "loopback-workspace".
A LoopBack workspace is essentially a container for application projects. It is not required to create an application, but may be helpful for organization.
slc lb project app_name
Creates a LoopBack application called appname, where appname is a valid JavaScript identifier. This command creates a new directory called appname in the current directory containing:
slc lb model modelname
Creates a model named modelname in an existing LoopBack application.
Provide the
-i
or --interactive
flag to be prompted through model
configuration. Use the --data-source
flag to specify the name of a
custom data source; default is data source named "db".
LoopBack APIs accept type descriptions remote methods, loopback.createModel()). The following is a list of supported types.
null
- JSON nullBoolean
- JSON booleanNumber
- JSON numberString
- JSON stringObject
- JSON objectArray
- JSON arrayDate
- a JavaScript date objectBuffer
- a node.js Buffer objectCreate a Loopback application.
var loopback = require('loopback');
var app = loopback();
app.get('/', function(req, res){
res.send('hello world');
});
app.listen(3000);
Notes
- extends express
- see express docs for details
- supports express / connect middleware
Initialize an application from an options object or a set of JSON and JavaScript files.
What happens during an app boot?
options.dataSources
object or datasources.json
in the current directoryoptions.models
object or models.json
in the current directory./models
directory are loaded with require()
.Options
cwd
- optional - the directory to use when loading JSON and JavaScript filesmodels
- optional - an object containing Model
definitionsdataSources
- optional - an object containing DataSource
definitionsNOTE: mixing
app.boot()
andapp.model(name, config)
in multiple files may result in models being undefined due to race conditions. To avoid this when usingapp.boot()
make sure all models are passed as part of themodels
definition.
The following is an example of an object containing two Model
definitions: "location" and "inventory".
{
"dealership": {
// a reference, by name, to a dataSource definition
"dataSource": "my-db",
// the options passed to Model.extend(name, properties, options)
"options": {
"relationships": {
"cars": {
"type": "hasMany",
"model": "Car",
"foreignKey": "dealerId"
}
},
"remoteMethods": {
"nearby": {
"description": "Find nearby locations around the geo point",
"accepts": [
{"arg": "here", "type": "GeoPoint", "required": true, "description": "geo location (lat & lng)"}
],
"returns": {"arg": "locations", "root": true}
}
}
},
// the properties passed to Model.extend(name, properties, options)
"properties": {
"id": {"id": true},
"name": "String",
"zip": "Number",
"address": "String"
}
},
"car": {
"dataSource": "my-db"
"properties": {
"id": {
"type": "String",
"required": true,
"id": true
},
"make": {
"type": "String",
"required": true
},
"model": {
"type": "String",
"required": true
}
}
}
}
Model Definition Properties
dataSource
- required - a string containing the name of the data source definition to attach the Model
tooptions
- optional - an object containing Model
optionsproperties
optional - an object defining the Model
properties in LoopBack Definition LanguageDataSource Definition Properties
connector
- required - the name of the connectorDefine a Model
and export it for use by remote clients.
// declare a DataSource
app.boot({
dataSources: {
db: {
connector: 'mongodb',
url: 'mongodb://localhost:27015/my-database-name'
}
}
});
// describe a model
var modelDefinition = {dataSource: 'db'};
// create the model
var Product = app.model('product', modelDefinition);
// use the model api
Product.create({name: 'pencil', price: 0.99}, console.log);
Note - this will expose all shared methods on the model.
You may also export an existing Model
by calling app.model(Model)
like the example below.
All models are avaialbe from the loopback.models
object. In the following
example the Product
and CustomerReceipt
models are accessed using
the models
object.
NOTE: you must call
app.boot()
in order to build the app.models object.
var loopback = require('loopback');
var app = loopback();
app.boot({
dataSources: {
db: {connector: 'memory'}
}
});
app.model('product', {dataSource: 'db'});
app.model('customer-receipt', {dataSource: 'db'});
// available based on the given name
var Product = app.models.Product;
// also available as camelCase
var product = app.models.product;
// multi-word models are avaiable as pascal cased
var CustomerReceipt = app.models.CustomerReceipt;
// also available as camelCase
var customerReceipt = app.models.customerReceipt;
Get the app's exported models. Only models defined using app.model()
will show up in this list.
var models = app.models();
models.forEach(function (Model) {
console.log(Model.modelName); // color
});
Enable swagger REST API documentation.
Options
basePath
The basepath for your API - eg. 'http://localhost:3000'.Example
// enable docs
app.docs({basePath: 'http://localhost:3000'});
Run your app then navigate to the api explorer. Enter your API basepath to view your generated docs.
Expose models over specified router.
For example, to expose models over REST using the loopback.rest
router:
app.use(loopback.rest());
View generated REST documentation by visiting: http://localhost:3000/_docs.
LoopBack comes bundled with several connect
/ express
style middleware.
Options
cookies
- An Array
of cookie namesheaders
- An Array
of header namesparams
- An Array
of param namesEach array is used to add additional keys to find an accessToken
for a request
.
The following example illustrates how to check for an accessToken
in a custom cookie, query string parameter
and header called foo-auth
.
app.use(loopback.token({
cookies: ['foo-auth'],
headers: ['foo-auth', 'X-Foo-Auth'],
cookies: ['foo-auth', 'foo_auth']
}));
Defaults
By default the following names will be checked. These names are appended to any optional names. They will always be checked, but any names specified will be checked first.
params.push('access_token');
headers.push('X-Access-Token');
headers.push('authorization');
cookies.push('access_token');
cookies.push('authorization');
NOTE: The
loopback.token()
middleware will only check for signed cookies.
A Loopback Model
is a vanilla JavaScript class constructor with an attached set of properties and options. A Model
instance is created by passing a data object containing properties to the Model
constructor. A Model
constructor will clean the object passed to it and only set the values matching the properties you define.
// valid color
var Color = loopback.createModel('color', {name: String});
var red = new Color({name: 'red'});
console.log(red.name); // red
// invalid color
var foo = new Color({bar: 'bat baz'});
console.log(foo.bar); // undefined
Properties
A model defines a list of property names, types and other validation metadata. A DataSource uses this definition to validate a Model
during operations such as save()
.
Options
Some DataSources may support additional Model
options.
Define A Loopbackmodel.
var User = loopback.createModel('user', {
first: String,
last: String,
age: Number
});
Require a model to include a property that matches the given format.
User.validatesFormat('name', {with: /\w+/});
Require a model to include a property to be considered valid.
User.validatesPresenceOf('first', 'last', 'age');
Require a property length to be within a specified range.
User.validatesLengthOf('password', {min: 5, message: {min: 'Password is too short'}});
Require a value for property
to be in the specified array.
User.validatesInclusionOf('gender', {in: ['male', 'female']});
Require a value for property
to not exist in the specified array.
User.validatesExclusionOf('domain', {in: ['www', 'billing', 'admin']});
Require a value for property
to be a specific type of Number
.
User.validatesNumericalityOf('age', {int: true});
Ensure the value for property
is unique in the collection of models.
User.validatesUniquenessOf('email', {message: 'email is not unique'});
Note: not available for all connectors.
Currently supported in these connectors:
Validate the model instance.
user.isValid(function (valid) {
if (!valid) {
console.log(user.errors);
// => hash of errors
// => {
// => username: [errmessage, errmessage, ...],
// => email: ...
// => }
}
});
An object containing a normalized set of properties supplied to loopback.createModel(name, properties)
.
Example:
var props = {
a: String,
b: {type: 'Number'},
c: {type: 'String', min: 10, max: 100},
d: Date,
e: loopback.GeoPoint
};
var MyModel = loopback.createModel('foo', props);
console.log(MyModel.properties);
Outputs:
{
"a": {type: String},
"b": {type: Number},
"c": {
"type": String,
"min": 10,
"max": 100
},
"d": {type: Date},
"e": {type: GeoPoint},
"id": {
"id": 1
}
}
Attach a model to a DataSource. Attaching a DataSource updates the model with additional methods and behaviors.
var oracle = loopback.createDataSource({
connector: require('loopback-connector-oracle'),
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
User.attachTo(oracle);
Note: until a model is attached to a data source it will not have any attached methods.
Mixins are added by attaching a vanilla model to a data source with a connector. Each connector enables its own set of operations that are mixed into a Model
as methods. To see available methods for a data source call dataSource.operations()
.
Log the available methods for a memory data source.
var ops = loopback
.createDataSource({connector: loopback.Memory})
.operations();
console.log(Object.keys(ops));
Outputs:
[ 'create',
'updateOrCreate',
'upsert',
'findOrCreate',
'exists',
'findById',
'find',
'all',
'findOne',
'destroyAll',
'deleteAll',
'count',
'include',
'relationNameFor',
'hasMany',
'belongsTo',
'hasAndBelongsToMany',
'save',
'isNewRecord',
'destroy',
'delete',
'updateAttribute',
'updateAttributes',
'reload' ]
Here is the definition of the count()
operation.
{
accepts: [ { arg: 'where', type: 'object' } ],
http: { verb: 'get', path: '/count' },
remoteEnabled: true,
name: 'count'
}
Note: These are the default mixin methods for a Model
attached to a data source. See the specific connector for additional API documentation.
Create an instance of Model with given data and save to the attached data source. Callback is optional.
User.create({first: 'Joe', last: 'Bob'}, function(err, user) {
console.log(user instanceof User); // true
});
Note: You must include a callback and use the created model provided in the callback if your code depends on your model being saved or having an id
.
Query count of Model instances in data source. Optional query param allows to count filtered set of Model instances.
User.count({approved: true}, function(err, count) {
console.log(count); // 2081
});
Find all instances of Model, matched by query. Fields used for filter and sort should be declared with {index: true}
in model definition.
filter
where Object
{ key: val, key2: {gt: 'val2'}} The search criteria
include String
, Object
or Array
Allows you to load relations of several objects and optimize numbers of requests.
order String
The sorting order
limit Number
The maximum number of instances to be returned
skip Number
Skip the number of instances
offset Number
Alias for skip
fields Object|Array|String
The included/excluded fields
['foo']
or 'foo'
- include only the foo property['foo', 'bar']
- include the foo and bar properties{foo: true}
- include only foo{bat: false}
- include all properties, exclude batFind the second page of 10 users over age 21 in descending order exluding the password property.
User.find({
where: {
age: {gt: 21}},
order: 'age DESC',
limit: 10,
skip: 10,
fields: {password: false}
},
console.log
);
Note: See the specific connector's docs for more info.
Delete all Model instances from data source. Note: destroyAll method does not perform destroy hooks.
Find instance by id.
User.findById(23, function(err, user) {
console.info(user.id); // 23
});
Find a single instance that matches the given where expression.
User.findOne({id: 23}, function(err, user) {
console.info(user.id); // 23
});
Update when record with id=data.id found, insert otherwise. Note: no setters, validations or hooks applied when using upsert.
Define a static model method.
User.login = function (username, password, fn) {
var passwordHash = hashPassword(password);
this.findOne({username: username}, function (err, user) {
var failErr = new Error('login failed');
if(err) {
fn(err);
} else if(!user) {
fn(failErr);
} else if(user.password === passwordHash) {
MyAccessTokenModel.create({userId: user.id}, function (err, accessToken) {
fn(null, accessToken.id);
});
} else {
fn(failErr);
}
});
}
Setup the static model method to be exposed to clients as a remote method.
loopback.remoteMethod(
User.login,
{
accepts: [
{arg: 'username', type: 'string', required: true},
{arg: 'password', type: 'string', required: true}
],
returns: {arg: 'accessTokenId', type: 'any'},
http: {path: '/sign-in'}
}
);
Note: These are the default mixin methods for a Model
attached to a data source. See the specific connector for additional API documentation.
Save an instance of a Model to the attached data source.
var joe = new User({first: 'Joe', last: 'Bob'});
joe.save(function(err, user) {
if(user.errors) {
console.log(user.errors);
} else {
console.log(user.id);
}
});
Save specified attributes to the attached data source.
user.updateAttributes({
first: 'updatedFirst',
name: 'updatedLast'
}, fn);
Remove a model from the attached data source.
model.destroy(function(err) {
// model instance destroyed
});
Define an instance method.
User.prototype.logout = function (fn) {
MyAccessTokenModel.destroyAll({userId: this.id}, fn);
}
Define a remote model instance method.
loopback.remoteMethod(User.prototype.logout)
Both instance and static methods can be exposed to clients. A remote method must accept a callback with the conventional fn(err, result, ...)
signature.
Expose a remote method.
Product.stats = function(fn) {
var statsResult = {
totalPurchased: 123456
};
var err = null;
// callback with an error and the result
fn(err, statsResult);
}
loopback.remoteMethod(
Product.stats,
{
returns: {arg: 'stats', type: 'object'},
http: {path: '/info', verb: 'get'}
}
);
Options
/products/stats
.Argument Description
An arguments description defines either a single argument as an object or an ordered set of arguments as an array.
// examples
{arg: 'myArg', type: 'number'}
[
{arg: 'arg1', type: 'number', required: true},
{arg: 'arg2', type: 'array'}
]
Types
Each argument may define any of the loopback types.
Notes:
Run a function before or after a remote method is called by a client.
// *.save === prototype.save
User.beforeRemote('*.save', function(ctx, user, next) {
if(ctx.user) {
next();
} else {
next(new Error('must be logged in to update'))
}
});
User.afterRemote('*.save', function(ctx, user, next) {
console.log('user has been saved', user);
next();
});
Remote hooks also support wildcards. Run a function before any remote method is called.
// ** will match both prototype.* and *.*
User.beforeRemote('**', function(ctx, user, next) {
console.log(ctx.methodString, 'was invoked remotely'); // users.prototype.save was invoked remotely
next();
});
Other wildcard examples
// run before any static method eg. User.find
User.beforeRemote('*', ...);
// run before any instance method eg. User.prototype.save
User.beforeRemote('prototype.*', ...);
// prevent password hashes from being sent to clients
User.afterRemote('**', function (ctx, user, next) {
if(ctx.result) {
if(Array.isArray(ctx.result)) {
ctx.result.forEach(function (result) {
result.password = undefined;
});
} else {
ctx.result.password = undefined;
}
}
next();
});
Remote hooks are provided with a Context ctx
object which contains transport specific data (eg. for http: req
and res
). The ctx
object also has a set of consistent apis across transports.
A Model
representing the user calling the method remotely. Note: this is undefined if the remote method is not invoked by a logged in user.
During afterRemote
hooks, ctx.result
will contain the data about to be sent to a client. Modify this object to transform data before it is sent.
When loopback.rest is used the following ctx
properties are available.
The express ServerRequest object. See full documentation.
The express ServerResponse object. See full documentation.
Define a "one to many" relationship.
// by referencing model
Book.hasMany(Chapter);
// specify the name
Book.hasMany('chapters', {model: Chapter});
Query and create the related models.
Book.create(function(err, book) {
// create a chapter instance
// ready to be saved in the data source
var chapter = book.chapters.build({name: 'Chapter 1'});
// save the new chapter
chapter.save();
// you can also call the Chapter.create method with
// the `chapters` property which will build a chapter
// instance and save the it in the data source
book.chapters.create({name: 'Chapter 2'}, function(err, savedChapter) {
// this callback is optional
});
// query chapters for the book using the
book.chapters(function(err, chapters) {
// all chapters with bookId = book.id
console.log(chapters);
});
book.chapters({where: {name: 'test'}, function(err, chapters) {
// all chapters with bookId = book.id and name = 'test'
console.log(chapters);
});
});
A belongsTo
relation sets up a one-to-one connection with another model, such
that each instance of the declaring model "belongs to" one instance of the other
model. For example, if your application includes users and posts, and each post
can be written by exactly one user.
Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
The code above basically says Post has a reference called author
to User using
the userId
property of Post as the foreign key. Now we can access the author
in one of the following styles:
post.author(callback); // Get the User object for the post author asynchronously
post.author(); // Get the User object for the post author synchronously
post.author(user) // Set the author to be the given user
A hasAndBelongsToMany
relation creates a direct many-to-many connection with
another model, with no intervening model. For example, if your application
includes users and groups, with each group having many users and each user
appearing in many groups, you could declare the models this way,
User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
user.groups(callback); // get groups of the user
user.groups.create(data, callback); // create a new group and connect it with the user
user.groups.add(group, callback); // connect an existing group with the user
user.groups.remove(group, callback); // remove the user from the group
Any static or instance method can be decorated as shared
. These methods are exposed over the provided transport (eg. loopback.rest).
A Loopback DataSource
provides Models with the ability to manipulate data. Attaching a DataSource
to a Model
adds instance methods and static methods to the Model
. The added methods may be remote methods.
Define a data source for persisting models.
var oracle = loopback.createDataSource({
connector: 'oracle',
host: '111.22.333.44',
database: 'MYDB',
username: 'username',
password: 'password'
});
Define a model and attach it to a DataSource
.
var Color = oracle.createModel('color', {name: String});
Discover a set of model definitions (table or collection names) based on tables or collections in a data source.
oracle.discoverModelDefinitions(function (err, models) {
models.forEach(function (def) {
// def.name ~ the model name
oracle.discoverSchema(null, def.name, function (err, schema) {
console.log(schema);
});
});
});
Discover the schema of a specific table or collection.
Example schema from oracle connector:
{
"name": "Product",
"options": {
"idInjection": false,
"oracle": {
"schema": "BLACKPOOL",
"table": "PRODUCT"
}
},
"properties": {
"id": {
"type": "String",
"required": true,
"length": 20,
"id": 1,
"oracle": {
"columnName": "ID",
"dataType": "VARCHAR2",
"dataLength": 20,
"nullable": "N"
}
},
"name": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "NAME",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"audibleRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "AUDIBLE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"effectiveRange": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "EFFECTIVE_RANGE",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"rounds": {
"type": "Number",
"required": false,
"length": 22,
"oracle": {
"columnName": "ROUNDS",
"dataType": "NUMBER",
"dataLength": 22,
"nullable": "Y"
}
},
"extras": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "EXTRAS",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
},
"fireModes": {
"type": "String",
"required": false,
"length": 64,
"oracle": {
"columnName": "FIRE_MODES",
"dataType": "VARCHAR2",
"dataLength": 64,
"nullable": "Y"
}
}
}
}
Enable remote access to a data source operation. Each connector has its own set of set remotely enabled and disabled operations. You can always list these by calling dataSource.operations()
.
Disable remote access to a data source operation. Each connector has its own set of set enabled and disabled operations. You can always list these by calling dataSource.operations()
.
// all rest data source operations are
// disabled by default
var oracle = loopback.createDataSource({
connector: require('loopback-connector-oracle'),
host: '...',
...
});
// or only disable it as a remote method
oracle.disableRemote('destroyAll');
Notes:
List the enabled and disabled operations.
console.log(oracle.operations());
Output:
{
find: {
remoteEnabled: true,
accepts: [...],
returns: [...]
enabled: true
},
save: {
remoteEnabled: true,
prototype: true,
accepts: [...],
returns: [...],
enabled: true
},
...
}
Create a data source with a specific connector. See available connectors for specific connector documentation.
var memory = loopback.createDataSource({
connector: loopback.Memory
});
Database Connectors
Other Connectors
Installing Connectors
Include the connector in your package.json dependencies and run npm install
.
{
"dependencies": {
"loopback-connector-oracle": "latest"
}
}
The built-in memory connector allows you to test your application without connecting to an actual persistent data source, such as a database. Although the memory connector is very well tested it is not recommended to be used in production. Creating a data source using the memory connector is very simple.
// use the built in memory function
// to create a memory data source
var memory = loopback.memory();
// or create it using the standard
// data source creation api
var memory = loopback.createDataSource({
connector: loopback.Memory
});
// create a model using the
// memory data source
var properties = {
name: String,
price: Number
};
var Product = memory.createModel('product', properties);
Product.create([
{name: 'apple', price: 0.79},
{name: 'pear', price: 1.29},
{name: 'orange', price: 0.59},
], count);
function count() {
Product.count(console.log); // 3
}
CRUD / Query
The memory connector supports all the standard query and crud operations to allow you to test your models against an in memory data source.
GeoPoint Filtering
The memory connector also supports geo-filtering when using the find()
operation with an attached model. See GeoPoint for more information on geo-filtering.
Use the GeoPoint
class.
var GeoPoint = require('loopback').GeoPoint;
Embed a latitude / longitude point in a Model.
var CoffeeShop = loopback.createModel('coffee-shop', {
location: 'GeoPoint'
});
Loopback Model's with a GeoPoint property and an attached DataSource may be queried using geo spatial filters and sorting.
Find the 3 nearest coffee shops.
CoffeeShop.attachTo(oracle);
var here = new GeoPoint({lat: 10.32424, lng: 5.84978});
CoffeeShop.find({where: {location: {near: here}}, limit:3}, function(err, nearbyShops) {
console.info(nearbyShops); // [CoffeeShop, ...]
});
Get the distance to another GeoPoint
.
var here = new GeoPoint({lat: 10, lng: 10});
var there = new GeoPoint({lat: 5, lng: 5});
console.log(here.distanceTo(there, {type: 'miles'})); // 438
Get the distance between two points.
GeoPoint.distanceBetween(here, there, {type: 'miles'}) // 438
Note: all distance methods use miles
by default.
miles
radians
kilometers
meters
miles
feet
degrees
The latitude point in degrees. Range: -90 to 90.
The longitude point in degrees. Range: -180 to 180.
For information on the LoopBack Android SDK, see Loopback Android. For API documentation, see LoopBack Android SDK API reference.
The REST API allows clients to interact with the LoopBack models using HTTP. The clients can be a web browser, a JavaScript program, a mobile SDK, a curl script, or anything that can act as an HTTP client.
LoopBack automatically binds a model to a list of HTTP endpoints that provide REST APIs for model instance data manipulations (CRUD) and other remote operations.
We'll use a simple model called Location
(locations for rental) to illustrate
what REST APIs are exposed by LoopBack.
By default, the REST APIs are mounted to /<pluralFormOfTheModelName>
, for
example, /locations
, to the base URL such as http://localhost:3000/.
For a model backed by a data source that supports CRUD operations, you'll see the following endpoints:
To expose a JavaScript method as REST API, we can simply describe the method as follows:
loopback.remoteMethod(
Location.nearby,
{
description: 'Find nearby locations around the geo point',
accepts: [
{arg: 'here', type: 'GeoPoint', required: true, description: 'geo location (lat & lng)'},
{arg: 'page', type: 'Number', description: 'number of pages (page size=10)'},
{arg: 'max', type: 'Number', description: 'max distance in miles'}
],
returns: {arg: 'locations', root: true},
http: {verb: 'POST', path: '/nearby'}
}
);
The remoting is defined using the following properties:
For POST and PUT requests, the request body must be JSON, with the Content-Type header set to application/json.
LoopBack uses the syntax from node-querystring to encode JSON objects or arrays as query string. For example,
user[name][first]=John&user[email]=callback@strongloop.com
==>
{ user: { name: { first: 'John' }, email: 'callback@strongloop.com' } }
user[names][]=John&user[names][]=Mary&user[email]=callback@strongloop.com
==>
{ user: { names: ['John', 'Mary'], email: 'callback@strongloop.com' }}
items=a&items=b
==> { items: ['a', 'b'] }
For more examples, please check out node-querystring
The response format for all requests is a JSON object or array if present. Some responses have an empty body.
Whether a request succeeded is indicated by the HTTP status code. A 2xx status code indicates success, whereas a 4xx status code indicates request related issues. 5xx status code reports server side problems.
The response for an error is in the following format:
{
"error": {
"message": "could not find a model with id 1",
"stack": "Error: could not find a model with id 1\n ...",
"statusCode": 404
}
}
###Generated APIs
###create
Create a new instance of the model and persist it into the data source
####Definition
POST /locations
####Arguments
####Example Request
curl -X POST -H "Content-Type:application/json"
-d '{"name": "L1", "street": "107 S B St", "city": "San Mateo", "zipcode": "94401"}'
http://localhost:3000/locations
####Example Response { "id": "96", "street": "107 S B St", "city": "San Mateo", "zipcode": 94401, "name": "L1", "geo": { "lat": 37.5670042, "lng": -122.3240212 } }
####Potential Errors
###upsert
Update an existing model instance or insert a new one into the data source
####Definition
PUT /locations
####Arguments
####Example Request
#####Insert
curl -X PUT -H "Content-Type:application/json"
-d '{"name": "L1", "street": "107 S B St", "city": "San Mateo", "zipcode": "94401"}'
http://localhost:3000/locations
#####Update
curl -X PUT -H "Content-Type:applicatin/json"
-d '{"id": "98", "name": "L4", "street": "107 S B St", "city": "San Mateo",
"zipcode": "94401"}' http://localhost:3000/locations
####Example Response
#####Insert { "id": "98", "street": "107 S B St", "city": "San Mateo", "zipcode": 94401, "name": "L1", "geo": { "lat": 37.5670042, "lng": -122.3240212 } }
#####Update { "id": "98", "street": "107 S B St", "city": "San Mateo", "zipcode": 94401, "name": "L4" }
####Potential Errors
###exists
Check whether a model instance exists by id in the data source
####Definition
GET /locations/exists
####Arguments
####Example Request curl http://localhost:3000/locations/88/exists
####Example Response
{
"exists": true
}
####Potential Errors
###findById
Find a model instance by id from the data source
####Definition
GET /locations/{id}
####Arguments
####Example Request curl http://localhost:3000/locations/88
####Example Response
{
"id": "88",
"street": "390 Lang Road",
"city": "Burlingame",
"zipcode": 94010,
"name": "Bay Area Firearms",
"geo": {
"lat": 37.5874391,
"lng": -122.3381437
}
}
####Potential Errors
###find
Find all instances of the model matched by filter from the data source
####Definition
GET /locations
####Arguments
filter The filter that defines where, order, fields, skip, and limit
where Object
{ key: val, key2: {gt: 'val2'}} The search criteria
include String
, Object
or Array
Allows you to load relations of several objects and optimize numbers of requests.
order String
The sorting order
limit Number
The maximum number of instances to be returned
skip Number
Skip the number of instances
offset Number
Alias for skip
fields Object|Array|String
The included/excluded fields
['foo']
or 'foo'
- include only the foo property
['foo', 'bar']
- include the foo and bar properties
{foo: true}
- include only foo
{bat: false}
- include all properties, exclude bat
For example,
'/weapons': Weapons
'/weapons?filter[limit]=2&filter[offset]=5': Paginated Weapons
'/weapons?filter[where][name]=M1911': Weapons with name M1911
'/weapons?filter[where][audibleRange][lt]=10': Weapons with audioRange < 10
'/weapons?filter[fields][name]=1&filter[fields][effectiveRange]=1': Only name and effective ranges
'/weapons?filter[where][effectiveRange][gt]=900&filter[limit]=3': The top 3 weapons with a range over 900 meters
'/weapons?filter[order]=audibleRange%20DESC&filter[limit]=3': The loudest 3 weapons
'/locations': Locations
'/locations?filter[where][geo][near]=153.536,-28.1&filter[limit]=3': The 3 closest locations to a given geo point
####Example Request
#####Find without filter curl http://localhost:3000/locations
#####Find with a filter curl http://localhost:3000/locations?filter%5Blimit%5D=2
Note: For curl, [
needs to be encoded as %5B
, and ]
as %5D
.
####Example Response
[
{
"id": "87",
"street": "7153 East Thomas Road",
"city": "Scottsdale",
"zipcode": 85251,
"name": "Phoenix Equipment Rentals",
"geo": {
"lat": 33.48034450000001,
"lng": -111.9271738
}
},
{
"id": "88",
"street": "390 Lang Road",
"city": "Burlingame",
"zipcode": 94010,
"name": "Bay Area Firearms",
"geo": {
"lat": 37.5874391,
"lng": -122.3381437
}
}
]
####Potential Errors
###findOne
Find first instance of the model matched by filter from the data source
####Definition
GET /locations/findOne
####Arguments
####Example Request curl http://localhost:3000/locations/findOne?filter%5Bwhere%5D%5Bcity%5D=Scottsdale
####Example Response
{
"id": "87",
"street": "7153 East Thomas Road",
"city": "Scottsdale",
"zipcode": 85251,
"name": "Phoenix Equipment Rentals",
"geo": {
"lat": 33.48034450000001,
"lng": -111.9271738
}
}
####Potential Errors
###deleteById
Delete a model instance by id from the data source
####Definition
DELETE /locations/{id}
####Arguments
####Example Request curl -X DELETE http://localhost:3000/locations/88
####Example Response
####Potential Errors
###count
Count instances of the model matched by where from the data source
####Definition
GET /locations/count
####Arguments
####Example Request
#####Count without where curl http://localhost:3000/locations/count
#####Count with a where filter curl http://localhost:3000/locations/count?where%5bcity%5d=Burlingame
####Example Response
{
count: 6
}
####Potential Errors
###nearby
Find nearby locations around the geo point
####Definition
GET /locations/nearby
####Arguments
lat
and lng
properties####Example Request curl http://localhost:3000/locations/nearby?here%5Blat%5D=37.587409&here%5Blng%5D=-122.338225
####Example Response
[
{
"id": "88",
"street": "390 Lang Road",
"city": "Burlingame",
"zipcode": 94010,
"name": "Bay Area Firearms",
"geo": {
"lat": 37.5874391,
"lng": -122.3381437
}
},
{
"id": "89",
"street": "1850 El Camino Real",
"city": "Menlo Park",
"zipcode": 94027,
"name": "Military Weaponry",
"geo": {
"lat": 37.459525,
"lng": -122.194253
}
}
]
####Potential Errors
###updateAttributes
Update attributes for a model instance and persist it into the data source
####Definition
PUT /locations/{id}
####Arguments
####Example Request
curl -X PUT -H "Content-Type:application/json" -d '{"name": "L2"}'
http://localhost:3000/locations/88
####Example Response { "id": "88", "street": "390 Lang Road", "city": "Burlingame", "zipcode": 94010, "name": "L2", "geo": { "lat": 37.5874391, "lng": -122.3381437 }, "state": "CA" }
####Potential Errors
###getAssociatedModel
Follow the relations from one model (location
) to another one (inventory
) to
get instances of the associated model.
####Definition
GET /locations/{id}/inventory
####Arguments
####Example Request curl http://localhost:3000/locations/88/inventory
####Example Response
[
{
"productId": "2",
"locationId": "88",
"available": 10,
"total": 10
},
{
"productId": "3",
"locationId": "88",
"available": 1,
"total": 1
}
]
####Potential Errors