Skip to main content

Foundation Getting Started

Getting Started With the Bluelibs Foundation

Intro​

The Bluelibs Foundation is a server-side JavaScript framework, built around an ecosystem of packages meant to help speed up development using a modern TypeScript, MongoDB, GraphQL stack. It provides high-level abstractions, called Bundles, on top of said packages, with a plug-and-play approach that allows you to focus on the specific business logic of your application. Let’s get started on how we can build a simple CRUD API with Bluelibs Foundation and explore some of the advantages. The followingh sections will dive deeper into each of its components and sections. If you are already familiar with the basics, feel free to skip this tutorial and dive deeper into our documentation, in the sections below.

Prerequisites​

Knowledge of JavaScript and TypeScript. Understanding MongoDB and GraphQL. A solid knowledge of object-oriented programming is recommended.

Installation​

Since we love TypeScript, the first thing we would like to install if you haven’t already is:

sudo npm install --location=global ts-node typescript '@types/node'

And then, to install the Bluelibs Foundation in your desired project directory:

npm install –-save @bluelibs/core 

Hello World​

Let us create an index.ts file from which to write and run our first, “hello world” example. First, we will need to import the Kernel and Bundle classes from @bluelibs/core and instantiate a Kernel.

import { Kernel, Bundle } from "@bluelibs/core";
const kernel = new Kernel();

The Kernel acts like a starting point for our entire system. Its purpose is to register the bundles and run their lifecycle methods. We will discuss more about them in the upcoming sections. Since our purpose now is to console.log a “hello world” message, we can say that is our application business logic. The development pattern is to encapsulate our logic in an AppBundle as below and then register it in the kernel:

class AppBundle extends Bundle {   // @ts-ignore   async init() { console.log("Hello world!") };}
kernel.addBundle(new AppBundle());

As you can see, Bundles are classes with specific lifecycle methods. This ensures that anything we would like to do is properly set up with required dependencies, configs, and setup. The only remaining step is to run the kernel:

kernel.init().then(() => {   console.log("You are now developing with Bluelibs!");})

In your terminal:

> ts-node index.ts                                                                                                                                                                                                                      Hello world!You are now developing with Bluelibs!

Congratulations! You have taken your first step into the Bluelibs world!

Bundles And Services​

As can be intuited, bundles are self-contained modules that encapsulate a specific domain of logic regarding your application, such as connecting to a database, serving an API, your custom business logic, security and more! The Bluelibs ecosystem already provides a couple of plug-and-play bundles that you can npm install and use in your application! With very little configuration, you are free to focus on your AppBundle, while the rest of Bluelibs deals with pretty much anything else you need. In this tutorial, we will use our Mongo and GraphQL Bundles to build a CRUD API, but before we move on with the code, let’s explore some core concepts regarding bundles and services.

Bundles​

As seen, Bundles are classes at a code-level. They modularize logic, services, and collections. In order to make sure that all dependencies, resources (connection to database, for example) and configurations are properly set up, they provide lifecycle methods, which we will briefly discuss below.

Bundles can depend on one another. This is why they expose the .extend() method, to be used with addDependency(). In the process, the dependency bundle will be added to the kernel, instantiated and have its own life cycle methods called. The services are the most important components of bundles. Since we adhere to OOP principles, services are singletons to be used via dependency injection. We use typeDI and its Container under the hood and you can refer to our documentation for examples. A bundle has a .prepare() method where we can set tokens in the container, for later use. The last life cycle method to be called is .init(). Here we do a “warmup” of the services which instantiates them and calls their .init() method if it exists. As per our documentation, “this is [also] where bundles usually start event loops (you start express) or connect to the database”.

Bundles can also be passed arguments that are needed downstream.

To reiterate, the whole bundle initialization and life cycle methods are there to ensure that the services have what they need.

Services​

We refer to “services” in the sense commonly known within the paradigm of (n-tier) layered architecture. A service is a class that encapsulates and abstracts the interaction with various entities. Thus, the core flow of our application logic comes from different services being called and interacting with one another. Services are inject-able singletons, managed by typeDI. They are the core components of bundles. Services from one bundle can be imported to another, creating coupling. However, if the bundle we are trying to use a service from is not registered in the kernel or added as a dependency to another bundle, the service may not be instantiated and configured properly!

Setting Up Mongo​

Now that we have an understanding of bundles and services, we can start to integrate our database logic! Bluelibs already provides a MongoBundle that we can install and use:

npm install --save @bluelibs/mongo-bundle

Let us assume we are trying to create a simple API for a blog app. To get started with the MongoBundle, what we first want to do is create a collection, add one element to it, and then query to check that we have successfully managed to persist data into mongo. The MongoBundle is very powerful and can be used to manage database transactions, build behaviors into our models, create migrations and much more! Please refer to our documentation to read more. For now, let us refactor what we have. A good first step would be to move the AppBundle in its own file.

// AppBundle.tsimport {Bundle} from "@bluelibs/core";
export default class AppBundle extends Bundle {   // @ts-ignore   async init() { console.log("Hello world!") };}
// index.tsimport { Kernel } from "@bluelibs/core";import AppBundle from "./AppBundle";
const kernel = new Kernel();

kernel.addBundle(new AppBundle());
kernel.init().then(() => {   console.log("You are now developing with Bluelibs!");})

Now that we want to work with a new bundle, we need to register it with the kernel. To avoid too many .addBundle() repetitive calls, we can use a slightly alternative syntax:

// index.tsimport { Kernel } from "@bluelibs/core";import AppBundle from "./AppBundle";import { MongoBundle } from "@bluelibs/mongo-bundle";
const kernel = new Kernel({   bundles: [       new MongoBundle({           uri: "mongodb://localhost:27017/bluelibs-blog"       }),       new AppBundle(),   ]});

kernel.init().then(() => {   console.log("You are now developing with Bluelibs!");})
Note: Since in our AppBundle we will want to do something that needs to connect to a mongo database, we add the MongoBundle first in the kernel! Order matters.

Make sure you have a (local) mongo instance running and that, when you instantiate the MongoBundle, you provide it with the correct URI pointing to your mongo database. We can start organizing our directory structure into /bundles and /services accordingly. Since we know that, for now, our business logic is to add an entity to the database and then query the collection to test the connection, we can also add a TestConnectionService and define our first model. What is a blog without posts? So let us also create a PostsCollection which is empty for now. The new directory structure should look like this:

foundation-getting-started│   package.json|   index.ts│   package-lock.json│   └───bundles│   └───AppBundle│       │   AppBundle.ts│       │   index.ts|       |│       └───collections│       |   |   PostsCollection.ts|       |   |│       └───services│           |   TestConnectionService.ts│   └───node_modules

In more complex applications, we will have to define models and their respective fields and the Bluelibs MongoBundle allows us to do that, like we are used to with any ORM. For this example, however, we shall keep it simple and only take care of the collection itself. Since non-relational databases have flexible schemas, we will manage to add an entity anyway.

In our newly created PostsCollection type in the following:

// PostsCollection.tsimport { Collection } from "@bluelibs/mongo-bundle";
export class PostsCollection extends Collection {   static collectionName = 'posts';}

The Collection class we import from mongo-bundle is an abstraction we built on top of Mongo collections. The most important aspect about it is that it is in essence a service. It has CRUD methods already defined that we can use! We extend it to manage a specific collection we are interested in and we provide the actual name we want the collection to have in the database.

Custom Service​

Since this is enough to get us started, let us now move on to our custom service:

// TestConnectionService.tsimport { Service, Inject } from "@bluelibs/core";import { PostsCollection } from "../collections/PostsCollection";
@Service()export class TestConnectionService {
   @Inject(() => PostsCollection)   private postsCollection: PostsCollection;
   constructor() {  }}

We decorate our service with @Service() and inject another service (our previously created collection) with @Inject(). Please refer to typeDI for a more detailed explanation of how this mechanism works. Now, we can add the logic:

async testConnection() {   await this.postsCollection.insertOne({       title: 'Learning BlueLibs',       description: 'I enjoy BlueLibs a lot!'   });   const results = await   this.postsCollection.find().toArray();   return results;}

We simply add something to our collection, then query it to make sure it’s there. This is all our custom service has to do, in order to validate everything works as intended. Now, this custom service will be part of our AppBundle, since that is where, at least for smaller applications, we keep our business logic. In order to have it work, our bundle needs to do a “warmup” of this service (making sure it is prepared and available to us, and calling its init method, if we have defined one). We can call this thisConnection() method as part of the init of our bundle. Let’s see how that should look like:

// AppBundle.tsimport { Bundle } from "@bluelibs/core";import { TestConnectionService } from "./services/TestConnectionService";

export class AppBundle extends Bundle {   // @ts-ignore   async init() {       await this.warmup([TestConnectionService]);
       const testConnectionService: TestConnectionService =         this.container.get(TestConnectionService);
       const results = await testConnectionService.testConnection();       console.log(results);   };}

That’s it! Running

ts-node index.ts

Will return:

[  {    _id: new ObjectId("6371fb608c5ee6d7d24c7891"),    title: 'Learning BlueLibs',    description: 'I enjoy BlueLibs a lot!'  }]You are now developing with Bluelibs!

Serving An API With GraphQL​

Now that we have everything we need, we can add the final piece of our introductory puzzle to serve a BlueLibs API with GraphQL. Of course, we can add REST endpoints as well using express under the hood, everyone’s favorite NodeJS framework, but BlueLibs has amazing support for GraphQL and offers two bundles to help us get everything done as fast as possible: the GraphQL Bundle and the Apollo Bundle.

First, let us install the bundles:

npm install --save graphql @bluelibs/graphql-bundle @bluelibs/apollo-bundle

Then, we register these new bundles with our Kernel in index.ts. It will now look like this:

// index.tsimport { Kernel } from "@bluelibs/core";import { MongoBundle } from "@bluelibs/mongo-bundle";import { AppBundle } from "./bundles/AppBundle";import { GraphQLBundle } from "@bluelibs/graphql-bundle";import { ApolloBundle } from "@bluelibs/apollo-bundle";
const kernel = new Kernel({   bundles: [       new MongoBundle({           uri: "mongodb://localhost:27017/bluelibs-blog"       }),       new GraphQLBundle(),       new ApolloBundle(),       new AppBundle(),   ]});

kernel.init().then(() => {   console.log("You are now developing with Bluelibs!");})

The GraphQLBundle extracts all graphql related files from a special directory called /graphql```` and it returns resolvers and type definitions. The ApolloBundleis mainly responsible for starting servers. It uses the loader from theGraphQLBundle```, so that is why we add it second.

Note: this section does not provide an in-depth look at graphql. For developers coming from a REST background, please make sure you familiarize yourself with the technology first.

Create a new directory under AppBundle, next to /collections and /services, which can be called graphql and move to that directory. We will be writing the graphql pieces necessary to handle our only collection, “posts”. The simple functionality we want to add for now is to see all the posts in our database and add a new post. For that we will need two main files, one for defining our types and another for defining our resolvers. Since we want our GraphQLBundle to parse them automatically, the names we give to these files is important: for type definitions we will have <collection_name>.graphql.ts and for resolvers we will have <collection_name>.resolvers.ts.

So let us create them!

In posts.graphql.ts we will have the following logic:

// AppBundle/graphql/posts.graphql.tsexport default `   type Post {       _id: ID       title: String       description: String   }     type Query {       getPosts: [Post]!   }     type Mutation {       addPost(input: AddPostInput!): Post!   }     input AddPostInput {       title: String       description: String   }`

This will create the type definitions we need for the simple functionality mentioned above.

Next, we need to define our resolvers, in posts.resolvers.ts. We define resolvers for the queries and mutations that we defined above. Resolvers are considered an array of executors, where you can provide a chain of actions to happen for that particular query or mutation. The Bluelibs Foundation provides a lot of flexibility, including the ability of having special, “middleware” resolvers apply before all others.

Note: if the following ResolverMap definition seems a tad overwhelming, not to worry! Please refer to our full documentation for a much more in depth explanation of what is happening behind the hood! Also, the Bluelibs Foundation is the stepping stone for two other amazing tools that we have built, X-Framework and Blueprint, where most of this is generated automagically for you! Check out our documentation and other tutorials to find out more.
import { IGraphQLContext, IResolverMap } from "@bluelibs/graphql-bundle";import { PostsCollection } from "../collections/PostsCollection";
export default {   Query: [       [], // executors that apply before all resolvers       {           getPosts: [               async (_: any, args: any, ctx: IGraphQLContext, ast: any) => {                   const postsCollection = ctx.container.get(PostsCollection);
                   return postsCollection.find().toArray();               }           ] // array of executors       }   ],
   Mutation: [       [],       {           addPost: [               async (_: any, args: any, ctx: IGraphQLContext, ast: any) => {                   const postsCollection = ctx.container.get(PostsCollection);
                   const response = await postsCollection.insertOne(args.input);
                   return postsCollection.findOne({_id: response.insertedId});               }           ]       }   ]} as IResolverMap

Note that both the Query and the Mutation properties are arrays. For their first element, we pass an empty array since we are not interested in that functionality at this point. The second element is an object containing the resolvers for our queries and mutations respectively. Please note that they are named exactly as specified in our type definitions. They are arrays of functions, since we mentioned that a resolver is a chain of executors. The function signature is also special:

async (_: any, args: any, ctx: IGraphQLContext, ast: any)

We are especially interested in args, since it has the input property with whatever was passed as input for the mutation and the ctx argument, which can give us access to the container. Remember that the container is the way for us to access services. Since our mongo collections have all the CRUD we need already implemented and act as services, we simply use them!

Under the /graphql directory we will now create a third file, called index.ts and add the following logic to it:

import { extract } from "@bluelibs/graphql-bundle";
export default extract(__dirname);

This will automagically parse our directory to extract the type definitions and resolvers but in order to pass them on to the server, we need to add a little modification to our AppBundle.ts.

// AppBundle.tsimport { Bundle } from "@bluelibs/core";import graphqlModule from "./graphql";import { Loader } from "@bluelibs/graphql-bundle";

export class AppBundle extends Bundle {   // @ts-ignore   async init() {};
   async prepare() {       const loader = this.container.get(Loader);       loader.load(graphqlModule);   };}

We import the /graphql extracted module we just worked on and pass it to the Loader from the GraphQLBundle.

Run It!​

And that is all! You can now go to your project root directory and run:

ts-node index.ts

You can now interact with your API at http://localhost:4000/graphql using your favorite GraphQL API tool.

Congratulations! You have developed your first app with Bluelibs! However, this is just one small step in a much larger journey.

Where To Go From Here​

There are a lot of things missing from our example: security, an HTTP/express server, logging, advanced functionality for everything we have discussed and much more! Please refer to our full documentation to learn about all that. We have built an amazing framework on top of all this, called X-Framework and even a great tool for you to scaffold entire X-Framework applications with just a few lines of code! You can, of course, deep dive into those on our website. However, before we talk about building apps with X-Framework, there is one more really cool piece of backend technology we made that we think you should check out: our example is nice, but it would be much nicer if we could have collections that relate to one another, just like in a relational database! Follow our next tutorial, “Getting Started With Nova” (coming soon in the "Nova" section) and our Nova documentation to find out more!