In this post, we will be developing a small API system using Deno (A runtime for Javascript & Typescript developed by Ryan Dahl, Node.JS creator).
For our API system we will be using Mandarine.TS framework. This framework will help us code & save a lot of time in our design as it has built-in solutions such as Dependency Injection, HTTP Handlers & Routes, and more.
Goals
- Create an API which manages books.
- Provide GET & POST Routes
- Use Mandarine.TS to get you involved with this framework and why it can be important when using Deno for web applications.
Let's get started.
Concepts
- Services: Classes Where our business logic is.
- Controllers: Classes responsible for handling HTTP requests as well as creating the routes for it.
- Mandarine.ts/Mandarine: A Typescript decorator-driven web framework for Deno.
Now, coding.
Step 1
We will be creating a tsconfig.json
file in the root of our project. Mandarine.TS framework requires a tsconfig.json
in order to work properly.
tsconfig.json
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false,
"noImplicitThis": false,
"alwaysStrict": false,
"strictNullChecks": false,
"strictFunctionTypes": true,
"strictPropertyInitialization": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"allowUmdGlobalAccess": false,
}
}
Step 2
We will be creating a typescript file and we will call it books-service.ts
. This file will contain the functioning of our API that we will later be using in our Controller. We will locate this file inside a folder called services
books-service.ts
import { Service } from "https://deno.land/x/mandarinets/mod.ts";
let books: Array<IBook> = new Array<IBook>();
interface IBook {
id: number;
name: string;
author: string;
}
@Service()
export class BooksService {
}
In code above, we are importing from the repository of Mandarine.TS framework the decorator @Service()
which will decorate our class BooksService
, this way, Mandarine can interpret this as a Mandarine-powered component. We have also declared a variable called books
which we will be using to add & get books from, and we have created an interface IBook
, which is simply the structure of the book item.
Step 3
Now that we have our service class decorated with @Service()
, we will be adding some logic to it, we will create 4 methods: addBook
, getBookById
, getBookByName
, getAllBooks
@Service()
export class BooksService {
public addBook(id: number, name: string, author: string) {
if(!books.some(book => book.name == name || book.id == id)) {
books.push({
id: id,
name: name,
author: author
});
}
}
public getBookById(id: number): IBook {
return books.find(book => book.id = id);
}
public getBookByName(name: string): IBook {
return books.find(book => book.name = name);
}
public getAllBooks(): Array<IBook> {
return books;
}
}
-
addBook
: Adds a book only when the id or the name are available. -
getBookById
: Look up a book in the array that matches with our provided id. -
getBookByName
: Look up a book in the array that matches with our provided book name. -
getAllBooks
: Returns all the books that have been added.
Step 4
In this step, we will be creating our controller. For this, we will create a new folder in the root of our project called controllers, and in this folder, we will locate our books-controller.ts which will contain our controller class.
books-controller.ts
import { Controller } from "https://deno.land/x/mandarinets/mod.ts";
@Controller('/api')
export class BooksController {
}
In the code above, we have declared our controller by using the decorator @Controller
which will inform Mandarine that this is a class that must be considered a controller (See official documentation here). As you can see, we are passing a parameter to our decorator with the value /api
, this means, all the routes inside our controller will have to start with /api
when requesting them.
Step 5
Now that we have declared our controller, we will inject our Service. This is done by importing our Service & putting it in the constructor of our controller.
Our code will look like this:
import { Controller } from "https://deno.land/x/mandarinets/mod.ts";
import { BooksService } from "../services/books-service.ts";
@Controller('/api')
export class BooksController {
constructor(private readonly booksService: BooksService){}
}
By injecting our service, Mandarine will resolve all its dependencies (if it has) and will make it available for us to use in our controller component without needing to manually initialize it. (See more documentation about this here)
Step 6
Now that we have our controller & we have also injected our book service (BooksService
), we will proceed to create 3 endpoints:
-
/api/books/add-sample
: It will add default books to our books array when requesting it. -
/api/books/getAll
: It will return all the books that have been added. -
/api/books/get
: It will get a book from the array by filtering by ID or book name.
Our controller will look like this
import { Controller, GET, POST, QueryParam } from "https://deno.land/x/mandarinets/mod.ts";
import { BooksService } from "../services/books-service.ts";
@Controller('/api')
export class BooksController {
constructor(private readonly booksService: BooksService){}
@POST('/books/add-sample')
public addSampleBooksHandler() {
this.booksService.addBook(1, "The Diary Of a Young Girl", "Anne Frank");
this.booksService.addBook(2, "Sapiens: A Brief History of Humankind", "Yuval Noah Harari");
return "Success";
}
@GET('/books/getAll')
public getAllBooks() {
return this.booksService.getAllBooks();
}
@GET('/books/get')
public getBook(@QueryParam('search') typeOfSearch: string, @QueryParam() data: string) {
if(typeOfSearch === 'id') {
// We use <any> so the compiler doesn't complain about using a possible string for a number parameter in getBookById
return this.booksService.getBookById(<any> data);
} else {
return this.booksService.getBookByName(data);
}
}
}
Note that we are importing 3 new items: GET
, POST
, and QueryParam
.
-
GET
: Will create a GET route. It takes one parameter which is the route to create. -
POST
: Will create a GET route. It takes one parameter which is the route to create. -
QueryParam
: It is a HTTP Parameter Decorator which will be responsible for injecting the values from the query parameters of the request into our HTTP handler. Refer to the link for a deeper explanation.
Step 7
Now that we have our service & controller for our books, we will proceed to create the entry point file. If you have got to this step, congratulations, this step is extremely easy & straight forward as we are just importing our classes to a common place.
This entry point file we will create is the file we will run with Deno
We will locate this file in the root of our project and we will call it entry-point.ts.
entry-point.ts
import { MandarineCore } from "https://deno.land/x/mandarinets/mod.ts";
import { BooksService } from "./services/books-service.ts";
import { BooksController } from "./controllers/books-controller.ts";
const services = [BooksService];
const controllers = [BooksController];
new MandarineCore().MVC().run();
In the code above, we are basically locating our components in just one place.
MandarineCore
is possibly the most important part for your code as it handles the engine of Mandarine Framework. This engine will resolve our dependencies & run our website. (See more here).
Step 8: Running our Mandarine-powered API.
To run our web application, we will run the following command in the root of our project.
deno run --config tsconfig.json --allow-read --allow-net entry-point.ts
After running that, we should be seeing our engine started which would mean we are ready to request our API.
Step 9: Requesting our API
/api/books/add-sample:
POST /api/books/add-sample HTTP/1.1
Host: localhost:8080
which will return Success
.
/api/books/getAll
GET /api/books/getAll HTTP/1.1
Host: localhost:8080
which will return
[
{
"id": 1,
"name": "The Diary Of a Young Girl",
"author": "Anne Frank"
},
{
"id": 2,
"name": "Sapiens: A Brief History of Humankind",
"author": "Yuval Noah Harari"
}
]
/api/books/get?search=id&data=2
GET /api/books/get?search=id&data=2 HTTP/1.1
Host: localhost:8080
which will return
{
"id": "2",
"name": "The Diary Of a Young Girl",
"author": "Anne Frank"
}
Summary
What we did
- We created an API using Deno.
- We created an API using Mandarine.TS as our framework.
- We interacted with Dependency Injection
- We worked with nested routes (Base route in controller to its children)
- We tested our endpoints.
Why Mandarine.ts was used as the web framework
- Mandarine saves us a lot of code & functionalities to build any type of web application. This time we used it for an API and in a few lines of code we did a lot with it (Services, Controllers, Handlers, Dependency Injection).
- Mandarine is decorator-driven, which means, it uses a lot of decorators to interpret functionalities which makes our code more readable and shorter.
Note
The intention of this article is to show how to build an API with Deno. The examples shown are simple examples that are not meant to be used in production environments.
Disclaimer
I am the creator of Mandarine.TS Framework.
The end
Get the full source code by clicking here.
Do you have any question or something to say? If so, please leave a comment. If you like this article, please tweet it!
Top comments (3)
Hi great article, it looks very promising Mandarine.TS. I try to create a simple endpoint by following your instructions and i got these errors:
Fatal error [
Error: Another accept task is ongoing
at unwrapResponse ($deno$/ops/dispatch_json.ts:42:11)
at Object.sendAsync ($deno$/ops/dispatch_json.ts:93:10)
at async ListenerImpl.accept ($deno$/net.ts:63:17)
at async Server.acceptConnAndIterateHttpRequests (server.ts:212:14)
at async MuxAsyncIterator.callIteratorNext (mux_async_iterator.ts:30:31)
]
I clone your repo and try to run the app and also i got the same error. Do you know if a miss something. My deno version is
deno 1.1.2
v8 8.5.216
typescript 3.9.2
Pretty cool - does it scale?
Great job!!!