Whether you start a new project or want to get ready for the next hackathon: These tools will help you to get fast results and keep code maintainable and scalable at the same time. Teammates will better understand what your code does by reading less, but more descriptive code.
At lea.online we often have created similar implementations across apps when managing collections, methods, publications, file uploads and http routes. We also decided to abstract some of these recurring pattern and published them as Meteor packages under a free license so you can benefit from our efforts, too. 💪
Code splitting
Meteor supports to write code once and use it on the server AND on the client. It also supports exact code-splitting at build-time, which allows you to indicate, if code is only meant for server or client. Both features together allow for isomorphic code, which means that an object "looks" similar (same functions/api) but can be implemented to behave diferrent, specifically for each environment.
In such a case you don't want code to leak into the other environment, which could have negative side-effects like increased client bundle size (thus longer load times). To achieve that and keep the code tidy and readable, we have created some short and easy functions:
// returns a value only on a given architectureexportconstonServer=x=>Meteor.isServer?x:undefinedexportconstonClient=x=>Meteor.isClient?x:undefined// execute a function and return it's value only on a given architectureexportconstonServerExec=fn=>Meteor.isServer?fn():undefinedexportconstonClientExec=fn=>Meteor.isClient?fn():undefined
An example of code-splitting with direct assignment:
import{onServer,onClient}from'utils/arch'exportconstGreeting={name:onServer('John Doe'),// will be undefined on the clientage:onClient(42)// will be undefined on the server }
Note, that this example is note isomorphic. On the server name will be present but not on the client. Vice versa with age.
An example of isomorphic code using code-splitting with function execution would be:
Use the direct assignments if values are static or have no external dependencies or if you want to create non-isomorphic objects. Otherwise use the function-based assignments to prevent leaking dependencies into the other environment.
Creating Mongo collections in Meteor is already as fast and easy as it ever can be. 🚀 Beyond that you may also want to attach a schema to a given collection, attach hooks or define the collection as local.
In order to achieve all this at once you can leverage the power of our lea.online collection factory package. Add the following packages to your project:
The other two packages (simpl-schema and aldeed:collection2) allow to validate any incoming data on the collection level (insert/update/remove), providing an extra layer of safety when it comes to handling your data. Note, that they are entirely optional.
Use-case
Let's consider a collection definition as an Object with a name (representing the collection name) and schema (representing the collection data structure schema):
As you can see there is no code for actually creating (instantiating) the collection and neither for creating a new schema. But it looks readable and clear to understand. This is where the factory comes into play:
With this function you have now a single handler to create collections and attach a schema to them:
// server/main.jsimport{Todos}from'../imports/api/Todos'import{createCollection}from'/path/to/createCollection'constTodosCollection=createCollection(Todos)TodosCollection.insert({foo:'bar'})// throws validation error, foo is not in schema
Collection instances
Separating definition from implementation is what makes your code testable and maintainable. You can even go one step further and access these collections independently (decoupled) from the local collection variable.
🗂 Meteor package allowing Mongo Collection instance lookup by collection name
Meteor Mongo Collection Instances
This package augments Mongo.Collection (and the deprecated Meteor.Collection) and allows you to later lookup a Mongo Collection instance by the collection name.
A central concept of Meteor is to define rpc-style endpoints, named Meteor Methods. They can be called by any connected clients, which makes them a handy way to communicate with the server but also an easy attack vector. Many concepts around methods exist to validate incoming data or restrict access.
We published leaonline:method-factory as a way to easily define methods in a mostly declarative way. It does so by extending the concept of mdg:validated-method by a few abstractions:
If you don't have Simple Schema installed, you need to add it via npm:
$meteornpminstall--savesimpl-schema
Defining a method for our Todos is super easy:
Todos.methods={create:{name:'todos.methods.create',schema:Todos.schema,// no need to write validate functionisPublic:false,// see mixins sectionrun:onServer(function (document){document.userId=this.userId// don't let clients decide who owns a documentreturngetCollection(Todos.name).insert(document)})}}
Creating the Method is similar to the previously mentioned collection factory:
Let's say you want to make Todos private and let only their owners create/read/update/delete them. At the same time you want to log any errors that occur during a method call or due to permission being denied. You can use mixins - functions that extend the execution of a method, to get to these goals:
exportconstcheckPermissions=options=>{const{run,isPublic}=options// check if we have an authenticated useroptions.run=function (...args){constenv=this// methods, flagged as isPublic have no permissions checkif (!isPublic&&!env.userId){thrownewMeteor.Error('permissionDenied','notLoggedIn')}// if all good run the original functionreturnrun.apply(env,args)}returnoptions}
// note: replace the console. calls with// your custom logging libraryexportconstlogging=options=>{const{name,run}=optionsconstlogname=`[${name}]:`options.run=function (...args){constenv=thisconsole.log(logname,'run by',env.userId)try{run.apply(env,args)}catch (runtimeError){console.error(logname,'error at runtime')throwruntimeError}}returnoptions}
This is how the updated method factory looks like using mixins:
These mixins are now applied to all methods automatically, without the need to assign them to each method on your own! Note, that the package would also allow to attach mixins only to a single method definition. If you want to read on the whole API documentation, you should checkout the repo:
With this package you can define factory functions to create a variety of Meteor methods
Decouples definition from instantiation (also for the schema) and allows different configurations for different
types of methods.
Minified size < 2KB!
Why do I want this?
Decouple definition from instantiation
Just pass in the schema as plain object, instead of manually instantiating SimpleSchema
Create fixed mixins on the abstract factory level, on the factory level, or both (see mixins section)
Installation
Simply add this package to your meteor packages
$ meteor add leaonline:method-factory
Usage
Import the createMethodFactory method and create the factory function from it:
import{createMethodFactory}from'meteor/leaonline:method-factory'constcreateMethod=createMethodFactory()// no params = use defaultsconstfancyMethod=createMethod({name: 'fancy',validate: ()=>{},run: ()=>
In Meteor you can subscribe to live updates of your Mongo collections. Meteor then takes care of all the syncing between server and the client. The server, however has to publish the data with all constraints as with methods (input validation, permissions etc.).
We created a handy abstraction for publications in order to have a similar API like the method factory (or like ValidatedMethod). It also allows you to pass mixins as with methods and this even reuse the mixins! Let's create our publication factory:
With this package you can define factory functions to create a variety of Meteor publications
Decouples definition from instantiation (also for the schema) and allows different configurations for different
types of publications.
Minified size < 2KB!
Why do I want this?
Decouple definition from instantiation
Validate publication arguments as with mdg:validated-method
Just pass in the schema as plain object, instead of manually instantiating SimpleSchema
Create mixins (similar to mdg:validated-method) on the abstract factory level, on the factory level, or both
(see mixins section)
Fail silently in case of errors (uses the publication's error and ready), undefined cursors or unexpected
returntypes
Installation
Simply add this package to your meteor packages
$ meteor add leaonline:publication-factory
Usage
Import the createPublicationFactory publication and create the factory function from it:
Every time you use Methods and Publications you should use Meteor's DDP rate limiter to prevent overloading server resources by massive calls to heavy methods or publications.
We provide with our ratelimiter factory a fast and effective way to include ratelimiting to your Methods, Publications and Meteor internals:
$ meteor add leaonline:ratelimit-factory
Then add the Method or Publication definitions to the rateLimiter:
import{Todos}from'/path/to/Todos'import{runRateLimiter,rateLimitMethod,rateLimitPublication}from'meteor/leaonline:ratelimit-factory'// ...Object.values(Todos.publications).forEach(options=>{createPublication(options)rateLimitPublication(options)})Object.values(Todos.methods).forEach(options=>{createMethod(options)rateLimitMethod(options)})runRateLimiter(function (reply,input){// if the rate limiter has forbidden a callif (!reply.allowed){constdata={...reply,...input}console.error('rate limit exceeded',data)}})
Under the hood the whole package uses DDPRateLimiter so you can also add numRequests and timeInterval to your Method or Publication definitions to get more fine-grained rate limits.
Creating HTTP endpoints is possible in Meteor but it operates at a very low-level, compared to Methods or Publications and can become very cumberstone with a growing code complexity.
At lea.online we tried to abstract this process to make it similar to defining Methods or Publications in a rather descriptive way (as shown in the above sections). The result is our HTTP-Factory:
import{WebApp}from'meteor/webapp'import{createHTTPFactory}from'meteor/leaonline:http-factory'importbodyParserfrom'body-parser'WebApp.connectHandlers.urlEncoded(bodyParser/*, options */)// inject body parserexportconstcreateHttpRoute=createHTTPFactory({schemaFactory:definitions=>newSimpleSchema(definitions)})
Now let's define an HTTP endpoint on our Todos:
Todos.routes={}Todos.routes.allPublic={path:'/todos/public',method:'get',schema:{limit:{type:Number,optional:true,min:1}},run:onServer(function (req,res,next){// use the api to get data instead if reqconst{limit=15}=this.data()returngetCollection(Todos).find({isPublic:true},{limit}).fetch()})}
Creating the endpoints at startup is, again, as easy as with the other factories:
With this package you can define factory functions to create a variety of Meteor HTTP routes
Decouples definition from instantiation (also for the schema) and allows different configurations for different
types of HTTP routes.
Meteor has no builtin concept to upload Files but there are great packages out there, suche as Meteor-Files (ostrio:files).
It supports uploading files to several storages, such as FileSystem, S3, Google Drive or Mongo's Builtin GridFs. The last one is a very tricky solution but provides a great way to upload files to the database without further need to register (and pay for) an external service or fiddling with paths and filesystem constraints.
Fortunately we created a package with complete GridFs integration for Meteor-Files:
import{MongoInternals}from'meteor/mongo'import{createGridFilesFactory}from'meteor/leaonline:grid-factory'import{i18n}from'/path/to/i8n'importfsfrom'fs'constdebug=Meteor.isDevelopmentconsti18nFactory=(...args)=>i18n.get(...args)constcreateObjectId=({gridFsFileId})=>newMongoInternals.NpmModule.ObjectID(gridFsFileId)constbucketFactory=bucketName=>newMongoInternals.NpmModule.GridFSBucket(MongoInternals.defaultRemoteCollectionDriver().mongo.db,{bucketName})constdefaultBucket='fs'// resolves to fs.files / fs.chunks as defaultconstonError=error=>console.error(error)exportconstcreateFilesCollection=createGridFilesFactory({i18nFactory,fs,bucketFactory,defaultBucket,createObjectId,onError,debug})
Now let's assume, our Todos app will have multiple users collaborating on lists and we want them to provide a profile picture then we can create a new FilesCollection with GridFs storage like so:
constProfileImages=createFilesCollection({collectionName:'profileImages',bucketName:'images',// put image collections in the 'images' bucketmaxSize:3072000,// 3 MB max in this examplevalidateUser:function (userId,file,type,translate){// is this a valid and registered user?if (!userId||Meteor.users.find(userId).count()!==1){returnfalse}constisOwner=userId===file.userIdconstisAdmin=...yourcodetodetermineadminconstisAllowedToDownload=...othercustomrulesif (type==='upload'){returnRoles.userIsInRole(userId,'can-upload','mydomain.com')// example of using roles}if (type==='download'){returnisOwner||isAdmin||isAllowedToDownload// custom flags}if (type==='remove'){// allow only owner to remove the filereturnisOwner||isAdmin}thrownewError(translate('unexpectedCodeReach'))}})
With this short setup you will save lots of time and effort that you would waste, when trying to get this whole GridFs setup running.
Create FilesCollections with integrated GridFS storage
Lightweight. Simple.
With this package you can easily create multiple ostrio:files collections
(FilesCollections) that work with MongoDB's
GridFS system out-of-the-box.
Background / reasons
It can be a real hassle to introduce gridFS as storage to your project.
This package aims to abstract common logic into an easy and accessible API while
ensuring to let you override anything in case you need a fine-tuned custom
behavior.
The abtract factory allows you to create configurations on a higher level that
apply to all your FilesCollections, while you still can fine-tune on the
collection level. Supports all constructor arguments of FilesCollection.
The great thing about all these tools is, that they can easily be combined into a single pipeline while the several definitions control, what is actually to be created:
import{createCollection}from'path/to/createCollection'import{createFilesCollection}from'path/to/createFilesCollection'import{createMethod}from'path/to/createMethod'import{createPublication}from'path/to/createPublication'import{createHttpRoute}from'path/to/createHttpRoute'exportconstcreateBackend=definitions=>{constcollection=createCollection(definitions)// files collections could be indicated by a files propertyif (definitions.files){createFilesCollection({collection,...definition.files})}// there will be no op if no methods are definedObject.values(definitions.methods||{}).forEach(options=>{createMethod(options)rateLimitMethod(options)})// there will be no op if no publications are definedObject.values(definitions.publications||{}).forEach(options=>{createMethod(options)rateLimitMethod(options)})// there will be no op if no publications are definedObject.values(definitions.routes||{}).forEach(options=>{createRoute(options)})}
Once you have setup this pipeline you can pass in various definitions, similar to the Todos object. The benefits of this approach will become more visible, once your application grows in terms of collections, methods and publications.
Some final notes
At lea.online we always try to improve our published code where we can. If you find any issues with the code in this article then please leave a comment and if you have trouble with the packages or miss crucial features then leave an issue in the repositories.
We hope these tools will help you boosting your productivity! 🚀
I regularly publish articles here on dev.to about Meteor and JavaScript. If you like what you are reading and want to support me, you can send me a tip via PayPal.
Keep up with the latest development on Meteor by visiting their blog and if you are the same into Meteor like I am and want to show it to the world, you should check out the Meteor merch store.