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-bundleLet 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!");})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_modulesIn 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.tsWill 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-bundleThen, 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.
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.
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 IResolverMapNote 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.tsYou 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!