Hi everyone, thanks for stopping by and welcome to my first post on dev.to. In this post we will look at creating a ToDo List API using Loopback 4 and MongoDB.
Introduction
I am a big fan of building backend APIs with NodeJS and Express, however I often find myself creating/copying a lot of the same code over and over again. I recently started a new project, and was looking for away to simply the creating of my API. There are a lot of backend frameworks to choose from, but I finally decided on Loopback 4.
Loopback has been around for a while, however they recently released version 4, which is a huge change from previous versions. In this post, we will look at creating a basic Todo list API using Loopback 4 and MongoDB.
Installing Loopback 4 CLI
The first think we have to do is install Loopback's CLI. Open a terminal and execute the following.
npm i -g @loopback/cli
This will install the latest version for us, which at the time of this writing was @loopback/cli version: 1.13.0
Create Project
After the CLI is installed, lets use it to create our application.
$ lb4 app
? Project name: todo-list
? Project description: ToDo List API written in Loopback 4 and MongoDB
? Project root directory: todo-list
? Application class name: TodoListApplication
? Select features to enable in the project (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Enable tslint: add a linter with pre-configured lint rules
◉ Enable prettier: install prettier to format code conforming to rules
◉ Enable mocha: install mocha to run tests
◉ Enable loopbackBuild: use @loopback/build helpers (e.g. lb-tslint)
◉ Enable vscode: add VSCode config files
◉ Enable docker: include Dockerfile and .dockerignore
◉ Enable repositories: include repository imports and RepositoryMixin
(Move up and down to reveal more choices)
I am settings the name of our application to todo-list, and giving it a description. We use the defaults for the rest of the options.
Loopback 4 Project Structure
Navigating through the project that was just created for us, there is a lot to take in. For now, let's focus on the key concepts of Loopback 4.
Concept | Description |
---|---|
Model | Similar to other frameworks, a model is used to define the format of a piece of data. Unlike other systems, you do not add your operations(create, read, update, delete) to the model itself. Personally, I like that the model definition and the operations are kept separately. |
DataSource | A DataSource is where our API is going to store/fetch data from. This could be in-memory storage, from a file, or in our case from a MongoDB database. |
Repository | This took me the longest to understand as it is the biggest change compared to other frameworks I have used. A repository is an abstraction layer between your model and your controller. The model defines the format of the data, the repository add the type of behavior you can do with the model, and the controller exposes the API endpoints and interacts with the repository. |
Controller | Compared to writing an Express API, the controller is where you put your API endpoint logic and handle requests/responses to your API. |
For more information on the folder structure, be sure the checking the official documentation here.
Models
We start our journey by creating our models. For our API, we are going to provide the user the ability to have different todo lists, and have multiple todo's inside each list. For this, we are going to create a Todo model and a TodoList model.
ToDo Model
Using Loopback's CLI, we can create the model by providing the model's name, type, and the properties inside our model.
Our Todo model will have the following properties
Property | Description |
---|---|
id | This will be the MongoDB ID. Note that we are setting it to type string |
title | this will be the title of our todo item |
description | This is an optional field, where the user can provide more details for the todo item |
isComplete | a boolean value when identifies if the item is complete or not |
Using the lb4 command in the terminal, we create our model like so ...
$ lb4 model
? Model class name: Todo
? Please select the model base class Entity (A persisted model with an ID)
? Allow additional (free-form) properties? No
Let's add a property to Todo
Enter an empty property name when done
? Enter the property name: id
? Property type: string
? Is id the ID property? Yes
? Is it required?: No
? Default value [leave blank for none]:
Let's add another property to Todo
Enter an empty property name when done
? Enter the property name: title
? Property type: string
? Is it required?: Yes
? Default value [leave blank for none]:
Let's add another property to Todo
Enter an empty property name when done
? Enter the property name: description
? Property type: string
? Is it required?: No
? Default value [leave blank for none]:
Let's add another property to Todo
Enter an empty property name when done
? Enter the property name: isComplete
? Property type: boolean
? Is it required?: No
? Default value [leave blank for none]:
Let's add another property to Todo
Enter an empty property name when done
? Enter the property name:
create src/models/todo.model.ts
update src/models/index.ts
Model Todo was created in src/models/
TodoList Model
We are going to do the same for our Todo List model.
$ lb4 model
? Model class name: TodoList
? Please select the model base class Entity (A persisted model with an ID)
? Allow additional (free-form) properties? No
Let's add a property to TodoList
Enter an empty property name when done
? Enter the property name: id
? Property type: string
? Is id the ID property? Yes
? Is it required?: No
? Default value [leave blank for none]:
Let's add another property to TodoList
Enter an empty property name when done
? Enter the property name: title
? Property type: string
? Is it required?: Yes
? Default value [leave blank for none]:
Let's add another property to TodoList
Enter an empty property name when done
? Enter the property name:
create src/models/todo-list.model.ts
update src/models/index.ts
Model TodoList was created in src/models/
Note that we are only specifying the id and title of the todo list. We will manually define the relationship between the todo list and todo later on. For now, lets continue with building out the boilerplate for our API.
Create MongoDB DataSource
Using Loopback's datasource command, we can tell our API where to store/fetch data from. For our example, we select MongoDB, and provide it with the connection information.
$ lb4 datasource
? Datasource name: db
? Select the connector for db: MongoDB (supported by StrongLoop)
? Connection String url to override other settings (eg: mongodb://username:password@hostname:port/database):
? host: localhost
? port: 27017
? user:
? password: [hidden]
? database: todo-list
? Feature supported by MongoDB v3.1.0 and above: Yes
create src/datasources/db.datasource.json
create src/datasources/db.datasource.ts
npm WARN todo-list@1.0.0 No license field.
+ loopback-connector-mongodb@4.2.0
added 10 packages from 14 contributors and audited 3961 packages in 14.925s
found 3 vulnerabilities (1 low, 2 moderate)
run `npm audit fix` to fix them, or `npm audit` for details
update src/datasources/index.ts
Datasource db was created in src/datasources/
Notice that the CLI automatically installs the loopback-connector-mongodb package for us. This is a library which loopbacks uses to communicate with MongoDB. This simplifies our code, because we don't have to handle connecting/disconnecting with the DB, nor do we have to worry about creating our own queries. This can a pro and a con at the same time.
Repositories
Up next is our repositories. Here, we are linking our datasource and our model together. Although outside of the scope of this tutorial, you have the ability to have a different datasource for each repository. This allows you to interact with data stored in different sources relatively easily.
TodoList Repository
$ lb4 repository
? Please select the datasource DbDatasource
? Select the model(s) you want to generate a repository TodoList? Please select the repository base class DefaultCrudRepository (Legacy juggler bridge)
create src/repositories/todo-list.repository.ts
update src/repositories/index.ts
Repository TodoListRepository was created in src/repositories/
Create ToDo Repository
$ lb4 repository
? Please select the datasource DbDatasource
? Select the model(s) you want to generate a repository Todo? Please select the repository base class DefaultCrudRepository (Legacy juggler bridge)
create src/repositories/todo.repository.ts
update src/repositories/index.ts
Repository TodoRepository was created in src/repositories/
Controllers
The last part is to generate our controllers. As part of our business logic, each todo must be linked to a todo list. To achieve this, we are going to create two controllers. One to handle our TodoList, and one to handle our todo's within each list. You will notice that we are not exposing a todo API directory. This is because we do not want to allow the user to create a todo item which is not assigned to a todo list.
TodoList Controller
$ lb4 controller
? Controller class name: TodoList
? What kind of controller would you like to generate? REST Controller with CRUD functions
? What is the name of the model to use with this CRUD repository? TodoList
? What is the name of your CRUD repository? TodoListRepository
? What is the type of your ID? string
? What is the base HTTP path name of the CRUD operations? /todo-lists
create src/controllers/todo-list.controller.ts
update src/controllers/index.ts
Controller TodoList was created in src/controllers/
Loopback's CLI walks us through the process by asking for the controller's name, the type of controller the repository it is linked to, the ID type, and the API endpoint we want to use.
TodoListTodo Controller
For this controller, we are going to choose an empty controller and code it ourselves. Because it will be nested within the TodoList controller, there are a few changes you will notice compared to the standard CRUD operations.
$ lb4 controller
? Controller class name: TodoListTodo
? What kind of controller would you like to generate? Empty Controller
create src/controllers/todo-list-todo.controller.ts
update src/controllers/index.ts
Controller TodoListTodo was created in src/controllers/
todo-list-todo.controller.ts
import {TodoListRepository} from './../repositories/todo-list.repository';
import {
repository,
Filter,
CountSchema,
Where,
Count,
} from '@loopback/repository';
import {
post,
requestBody,
param,
get,
patch,
getWhereSchemaFor,
del,
} from '@loopback/rest';
import {Todo} from '../models';
export class TodoListTodoController {
constructor(
@repository(TodoListRepository)
protected todoListRepo: TodoListRepository,
) {}
@post('/todo-lists/{id}/todos', {
responses: {
'200': {
description: 'TodoList.Todo model instance',
content: {
'application/json': {schema: {'x-ts-type': Todo}},
},
},
},
})
async create(
@param.path.string('id') id: string,
@requestBody() todo: Todo,
): Promise<Todo> {
return await this.todoListRepo.todos(id).create(todo);
}
@get('/todo-lists/{id}/todos', {
responses: {
'200': {
description: "Array of Todo's belonging to TodoList",
content: {
'application/json': {
schema: {type: 'array', items: {'x-ts-type': Todo}},
},
},
},
},
})
async find(
@param.path.string('id') id: string,
@param.query.object('filter') filter?: Filter,
): Promise<Todo[]> {
return await this.todoListRepo.findTodos(id);
// return await this.todoListRepo.todos(id).find(filter);
}
@patch('/todo-lists/{id}/todos', {
responses: {
'200': {
description: 'TodoList.Todo PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async patch(
@param.path.string('id') id: string,
@requestBody() todo: Partial<Todo>,
@param.query.object('where', getWhereSchemaFor(Todo))
where?: Where,
): Promise<Count> {
return await this.todoListRepo.todos(id).patch(todo, where);
}
@del('/todo-lists/{id}/todos', {
responses: {
'200': {
description: 'TodoList.Todo DELETE success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async delete(
@param.path.string('id') id: string,
@param.query.object('where', getWhereSchemaFor(Todo))
where?: Where,
): Promise<Count> {
return await this.todoListRepo.todos(id).delete(where);
}
}
And that's it! By using the Loopback 4's CLI, we were about to generate all of our API logic. I don't know about you, but that was a lot quicker than doing it by hand. There are a few manual things we have to do ourselves, which we will look at now.
Relationships between models
In our API, we need to define a one-to-many relationship between a todo list and a todo item. A todo list can have multiple todo items, but a todo item can only be assigned to a single todo list.
todo-list.model.ts
First, we are going to add modify our todo-list model by adding a hasMany property. Open the todo-list.model.ts, and modify it as follows
import {hasMany, Entity, model, property} from '@loopback/repository';
import {Todo} from './todo.model';
@model({})
export class TodoList extends Entity {
@property({
type: 'string',
id: true,
})
id?: string;
@property({
type: 'string',
required: true,
})
title: string;
@hasMany(() => Todo)
todos?: Todo[];
constructor(data?: Partial<TodoList>) {
super(data);
}
}
You'll notice that we are importing the hasMany property and our Todo model. We are also adding a new property called todos, which is an array of todo.
todo-list.repository.ts
We've told our model to use the type Todo. Now, we are going to import the Todo repository into our todo-list repo, so we can access the their underlying operations.
import {TodoRepository} from './todo.repository';
import {
DefaultCrudRepository,
HasManyRepositoryFactory,
repository,
} from '@loopback/repository';
import {TodoList, Todo} from '../models';
import {DbDataSource} from '../datasources';
import {inject, Getter} from '@loopback/core';
export class TodoListRepository extends DefaultCrudRepository<
TodoList,
typeof TodoList.prototype.id
> {
public readonly todos: HasManyRepositoryFactory<
Todo,
typeof TodoList.prototype.id
>;
constructor(
@inject('datasources.db') dataSource: DbDataSource,
@repository.getter(TodoRepository)
protected todoRepositoryGetter: Getter<TodoRepository>,
) {
super(TodoList, dataSource);
// We associate the todos property to be hasmany of type Todo.
this.todos = this.createHasManyRepositoryFactoryFor(
'todos',
todoRepositoryGetter,
);
}
}
todo.model.ts
The last step is tell the Todo which list it belongs to. We do this by adding a todoListId property to our todo model
import {Entity, model, property} from '@loopback/repository';
@model({settings: {}})
export class Todo extends Entity {
@property({
type: 'string',
id: true,
})
id?: string;
@property({
type: 'string',
required: true,
})
title?: string;
@property({
type: 'string',
})
description?: string;
@property({
type: 'boolean',
})
isComplete?: boolean;
@property()
todoListId: string;
constructor(data?: Partial<Todo>) {
super(data);
}
}
The MongoDB Quirk
There is currently a bug in the MongoDB connector, which returns a blank array when looking for nested information due to the id property not being converted in an object id. This effects our code because the normal way to look up the todos inside a todolist. To fix this, we are going to create our own find logic. Open todo-list.repository.ts one more time, and add the following function.
async findTodos(id: string): Promise<Todo[]> {
return await this.todoRepo.find().then(todos => {
return todos.filter(todo => {
return todo.todoListId === id;
});
});
}
Inside the todolist.controller.ts, we are going to modify our get method to use our new function. Modify the find function to the following.
async find(
@param.path.string('id') id: string,
@param.query.object('filter') filter?: Filter,
): Promise<Todo[]> {
return await this.todoListRepo.findTodos(id);
// return await this.todoListRepo.todos(id).find(filter);
}
To read more about the current bug, you can track is here
Testing
To start our API server, open your terminal and execute
npm start
You use other API testing tools list Postman, and you still can, but one of the features of Loopback is their API explorer, which provides you the ability to view and test your API. Check it out by navigating to localhost:3000 in your browser.
Conclusion
In just a few minutes, we were able to build a fully working API without having to write too much code. I am going to continue playing around with Loopback 4 and look forward to sharing my experiences.
What backend frameworks are you using? Have you checked out Loopback 4 yet?
The full code can be found on my GitHub, here.
This article was also posted on my website offhourscoding
Top comments (2)
Hi Matt,
Thank you for writing this article. This is very helpful.
I am currently using Loopback 4 and Azure cosmos db 3.6 with mongodb. I am not very sure you are familiar with azure cosmosdb. 3.6 is the latest version and it only automatically indexes _id field but not other fields in the collection. While 3.2 does index every fields. My question is how do I create an index in loopback 4 using the datasource or repository.
Thanks again.
This might be late, but here you can find all that you need (and if not, there's issues for you): github.com/strongloop/loopback-con...
But short answer might be "you can't", because this is just a connector (in contrast to mongoose that is an ORM between your backend and MongoDB). Hope it helps