When I started writing NodeJS APIs, I rapidly fell in love with NestJS which combines my knowledge of Angular and a strongly structured backend frameworks like Spring.
Today deno is almost there and we’ve already got something quite similar to Nest in the Deno world, it is called Alosaur !
It looks like it was inspired by NestJS as it brings all the good stuff:
- Modular architecture
- Dependency injection
- Angular schematics
- Decorator
- Validator
- Orm
- ...
Deno install
If you know nothing about deno, a good starting point could be the Olivier’s Dev.to post or you can install it with the following commands :
#shell
curl -fsSL https://deno.land/x/install/install.sh | sh
#powershell
iwr https://deno.land/x/install/install.ps1 -useb | iex
#homebrew
brew install deno
Don’t forget to add the environment variables to your .profile (or equivalent) and reload it if needed:
export DENO_INSTALL="/home/YOUR_NAME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
source ~/.profile # or equivalent
You should now be able to run deno from the console: deno -h
Init project
To initialize a new project you really don’t need more than creating a folder that contains a Typescript file eg:main.ts
You will be able to run your project by typing deno run main.ts
VSCode integration
When you start copy pasting some code from the documentation, you rapidly face IDE incompatibilities. Import statements are a good example:
While writing this article, there are 2 extensions for Deno. Axetroy’s extension seems to be the more sophisticated. You have to manually activate deno extension in the settings. The author advises you to do it for your workspace only.
Update Use the offical deno extension
You can create a .vscode/settings.json containing the following:
{
"deno.unstable": false,
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features",
},
"[typescriptreact]": {
"editor.defaultFormatter": "axetroy.vscode-deno",
},
"deno.enable": true,
}
At the same time, once we’re going to use nice features like decorators, we will have to tell VSCode how to handle them. The usual way to do this is by creating a tsconfig.json
at the root of your project:
{
"compilerOptions": {
"plugins": [
{
"name": "typescript-deno-plugin"
}
],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Start with Alosaur
As I said, Alosaur is very similar to NestJS. What we used to call modules are now called Areas. For example, you could create an empty MainArea with the following code:
import { App, Area } from 'https://deno.land/x/alosaur/src/mod.ts';
// Declare module
@Area({
})
export class MainArea {}
// Create alosaur application
const app = new App({
areas: [MainArea],
});
app.listen();
You will be able to run it by using the appropriate flags:
deno run --allow-net --config ./tsconfig.json main.area.ts
- allow-net is used to give permission to use network
- config specifies typescript configuration and especially experimental decorator
Alosaur controller
As for every app respecting separation of concern, a good starting point could be to implement a controller.
import { Controller, Get, Area, App } from 'https://deno.land/x/alosaur/src/mod.ts';
@Controller('/users')
export class UserController {
@Get('')
getAll() {
return [{id:1, name:"Jack"}];
}
}
As usual, we use the Controller decorator with the parameter “/users” to declare our class in charge of requests on the “/users” endpoint.
We also declare a getAll method decorated with a the Get decorator.
We now need to do add our freshly created controller into our MainArea:
@Area({
controllers: [UserController],
})
Don’t forget to import UserController correctly (with the .ts)
If you run your deno application now, you should be able to get some data on this url: http://localhost:8000/users
Service
A proper way to isolate business logic is by creating a service layer.
We can create a Service with the following:
// user.service.ts
export class UserService {
getAll() {
return [{ id: 1, name: "Jack" }];
}
}
We can now inject UserService directly inside UserController.
Using the constructor declaration is the shortest way to do it:
export class UserController {
constructor(private userService:UserService){}
@Get('')
getAll() {
return this.userService.getAll();
}
}
microsoft/tsyringe is included within Alosaur.
Tsyringe is described by Microsoft as : "A lightweight dependency injection container for TypeScript/JavaScript for constructor injection."
Alosaur's controllers are using DI by default and are therefore respecting the IoC (inversion of control) pattern.
Repository
What we want next is an abstraction for our data access. Repositories are commonly used for that purpose.
// user.repository.ts
export class UserRepository {
getAll(){
return [{ id: 1, name: "Jack" }];
}
}
We will use that repository exactly the same way that we did before with the UserService/UserController.
export class UserService {
constructor(private userRepository: UserRepository) {}
getAll() {
return this.userRepository.getAll();
}
}
If you run your app now, you're going to face:
error: Uncaught TypeInfo not known for class UserService
That's because the DI mechanism doesn't know how to deal with that UserRepository inside the constructor. Thankfully, you just have to decorate your UserService with AutoInjectable to solve the issue.
AutoInjectable replaces our constructor by "a parameterless constructor that has dependencies auto-resolved", isn't it beautiful ?
import { AutoInjectable } from "https://deno.land/x/alosaur/src/mod.ts";
import { UserRepository } from '../repository/user.repository.ts';
@AutoInjectable()
export class UserService {
constructor(private userRepository: UserRepository) {}
getAll() {
return this.userRepository.getAll();
}
}
TypeORM Entity
Jack is nice, but Jack is hardcoded into our repository. We want to get data from the database.
TypeORM is a powerful micro framework used to deal with databases.
"TypeORMis highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework."
In this article we're going to use a deno compatible fork.
The simplest way to start is by creating our first entity: user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'https://denolib.com/denolib/typeorm@v0.2.23-rc3/mod.ts';
@Entity()
export class User{
@PrimaryGeneratedColumn()
id!: number;
@Column("varchar", { length: 30 })
name!: string;
}
At the time when I'm writing this article, I have to hardcode typeorm's version. This could change in a near future.
- The Entity decorator indicates that the user class will be persisted (In relational database context, that will be a table)
- The PrimaryGeneratedColumn decorator describes our id property as the primary auto generated value (Auto increment id).
Primary key is mandatory.
TypeORM configuration
To configure the micro framework we need to pass the correct parameters to the createConnexion function.
// init-typeorm.ts
import { createConnection } from 'https://denolib.com/denolib/typeorm@v0.2.23-rc3/mod.ts';
export function initTypeORM() {
return createConnection({
type: "postgres", // mysql is not currently supported 19/05/2020
host: "172.17.0.2",
port: 5432,
username: "postgres",
password: "pwd",
database: "postgres", // default database
entities: [
"src/entities/*.ts"
],
synchronize: true,
});
}
To be detected by TypeORM, your user entity has to be at this location: src/entities/user.entity.ts
To launch the corresponding database on your computer: docker hub
docker run --name some-postgres -e POSTGRES_PASSWORD=pwd -d postgres
On my linux, I can get the container IP (172.17.0.2) by using docker inspect.
On Mac and windows, you sould be able to connect to the database by using the--publish=5432:5432
and then connect the db directly on localhost.
We now should call initTypeORM from our main area:
. . .
await initTypeORM(); // Init before creating the app
const app = new App({
areas: [MainArea],
});
app.listen();
If everything is configured correctly, we are now able to run our app. The "syncronize:true"
parameter will generate missing tables automatically.
Not so fast: we will have to specify some additional options to deno:
deno run --unstable --allow-net --allow-read --config ./tsconfig.json main.area.ts
unstable because at the current time, TypeORM uses some unstable feature of deno.
allow-read because TypeORM scans the filesystem to dynamically find entities.
Custom repository
The final step to use all the power of TypeORM is to create a custom repository. To do that, we are going to decorate our repository with EntityRepository and extend Repository :
import { EntityRepository } from "https://denolib.com/denolib/typeorm@v0.2.23-rc3/src/decorator/EntityRepository.ts";
import { Repository } from "https://denolib.com/denolib/typeorm@v0.2.23-rc3/src/repository/Repository.ts";
import { User } from '../entities/user.entity.ts';
@EntityRepository(User)
export class UserRepository extends Repository<User> {
}
Let's change our service to correctly use typeORM's find method:
import { AutoInjectable } from "https://deno.land/x/alosaur/src/mod.ts";
import { getCustomRepository } from "https://denolib.com/denolib/typeorm@v0.2.23-rc3/src/index.ts";
import { UserRepository } from '../repository/user.repository.ts';
@AutoInjectable()
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = getCustomRepository(UserRepository);
}
getAll() {
return this.userRepository.find();
}
}
At this point, our app is runnable without the AutoInjectable, but there are big chances that on a real app you will need a DI mechanism.
As you can see, we are now using getCustomRepository to get the instance of our repository. That's unfortunately not very consistent with our dependency injection from Microsoft but I didn't find anything better for now.
At this point, a GET request on http://localhost:8000/users returns an empty array.
However, If you manually add some data in the user table, you will correctly get it. You could also declare a second endpoint and use TypeORM to save a new user. (check my repository for more examples)
Conclusion
Deno, success or not ? Future will tell, but I hope this article will help people who would give it a try !
The full source code for the article: https://github.com/hugoblanc/deno-api
Top comments (0)