DEV Community

loading...
Cover image for Creating REST API's with Node, TypeScript and AdonisJS 5

Creating REST API's with Node, TypeScript and AdonisJS 5

fredmaiaarantes profile image Frederico Maia Arantes Originally published at fredmaia.dev Updated on ・10 min read

Why AdonisJS?

AdonisJS is a complete Node.JS framework highly focused on developer ergonomics, stability and speed. Created in 2015, inspired by frameworks like Laravel and Rails. AdonisJS 5 comes with ton of features like:

  • Type safety with first class support for TypeScript. <3

  • Edge, a template engine with all the features you need to construct dynamic webpages.

  • A robust SQL ORM with Query Builder, Seeds, Migrations and Active Record Models.

  • HTTP router and first class support for JSON:API.

  • Form validator that provides type information, along with the runtime validations.

  • Multi Driver Auth which let you choose between Sessions, Opaque tokens and JWT tokens.

  • Inbuilt health check module and strong emphasis on Web security.

We are going to create now a blog-api project with AdonisJS 5 to understand some of its concepts.

Pre-requisites

AdonisJS 5 requires Node.JS >= 12.0.0, along with NPM >= 6.0.0. We are going to use Yarn as our package manager and Visual Studio Code as our editor.

Creating a New Project

Run the command below to set up a new project structure and install all the required dependencies.

$ yarn create adonis-ts-app blog-api-adonisjs-5

Choose API Project in the boilerplate prompt and confirm the project name. I also recommend accepting to install ESLint.
The image below shows the project structure of AdonisJS, following Convention over Configuration, it serves as a great starting point to develop applications.
You can read more about it here.

Image of AdonisJS Project Structure

Enter the newly created directory and run the development server.

$ cd blog-api-adonisjs-5 && yarn start (it runs node ace serve --watch)

Open your browser on http://localhost:3333, and you should see a JSON 'Hello World'. This response is simple defined in start/routes.ts. Run the following command to create a production build.

$ yarn build (it runs node ace build --production)

You can view all the available commands by running node ace --help.

Creating a Controller

AdonisJS follows the MVC (Model-View-Controller) architecture and Controllers handle HTTP requests. Controllers live inside the app/Controllers/Http directory. The command below generates a new Post controller.

$ node ace make:controller Post

Listing all Posts

Open the project using VS Code and add an index method to the PostsController that returns an in memory array with all posts.

// file: app/Controllers/Http/PostsController.ts

export default class PostsController {
  public async index () {
    return [
      { id: 1, title: 'First Post', content: 'This is my first blog post' },
      { id: 2, title: 'Second Post', content: 'This is my second blog post' },
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Remove the current code from the routes.ts and add a new route for the index method. The code below defines a route to /posts using the GET method. The route handler references to the index method we just created.

//file: start/routes.ts

import Route from '@ioc:Adonis/Core/Route'

Route.get('posts', 'PostsController.index')
Enter fullscreen mode Exit fullscreen mode

If you have stopped the server, start it again running yarn start and access http://localhost:3333/posts.

Image of the browser listing Posts
Better than accessing from the browser, you can consume this API from a Rest Client like Postman. Postman is
a great tool and if you don't know it I suggest you start using it to test and / or document your REST API's.
https://www.postman.com/downloads/

Image of Postman listing Posts

Creating a new Post

Add a store method to the PostsController to receive the data to create a new post. The values will come from
the body of the HTTP request, to get the data we need to extract it from the request object.

// file: app/Controllers/Http/PostsController.ts
...
public async store ({ request }: HttpContextContract) {
    const data = request.all()
    console.log(data)
}
Enter fullscreen mode Exit fullscreen mode

The store method is pretty simple for the moment, it is just printing the data from the body using console.log(),
we will improve it later.

Add a new route to the store method. The code below defines a route to /posts using the POST HTTP method.

// file: start/routes.ts

import Route from '@ioc:Adonis/Core/Route'

Route.get('posts', 'PostsController.index')

Route.post('posts', 'PostsController.store') //new route
Enter fullscreen mode Exit fullscreen mode

Now we can use Postman to send a POST request to our API.
Before sending a POST request, add a header defining the Content-Type as application/json.

Image of Postman Adding JSON Header

With that we can send a request to the API to create a new post.

Image of Postman Creating a Post

Sending this request you can see on the console:

{ title: 'Third post', content: 'This is my third post' }
Enter fullscreen mode Exit fullscreen mode

Using request.all() works fine but this method will accept any coming data, for security reasons we should accept only the
fields we know that compose a post object, in this case title and content. Change the store method to
use request.only() instead of request.all().

// file: app/Controllers/Http/PostsController.ts
...
public async store ({ request }: HttpContextContract) {
    const data = request.only(['title', 'content'])
    console.log(data)
}
Enter fullscreen mode Exit fullscreen mode

If you make a GET request to /posts of course we cannot see the new post, we just printed it out. To make things a bit more interesting let's keep an array of posts in memory and add this new post to the array of posts.

// file: app/Controllers/Http/PostsController.ts

export default class PostsController {
  private static posts = [
    { id: 1, title: 'First Post', content: 'This is my first blog post' },
    { id: 2, title: 'Second Post', content: 'This is my second blog post' },
  ]

  public async index () {
    return PostsController.posts
  }

  public async store ({ request }: HttpContextContract) {
    const data = request.only(['title', 'content'])
    const newId = PostsController.posts.length + 1
    const post = {
      id: newId,
      title: data.title,
      content: data.content,
    }
    PostsController.posts.push(post)
    return post
  }
}
Enter fullscreen mode Exit fullscreen mode

Now if we send a POST request adding the third post, and then we make a GET request to see all of them, we will be able
to see the new created post.

Removing a Post

Add a destroy method to the PostsController. To delete a post we
will send a DELETE request to /posts/:id for instance /posts/1 passing the post id as the parameter. To get the
id parameter we must receive a params object and extract from it. Once we have the id as a number we filter the
array of posts to remove the object with the informed id.

// file: app/Controllers/Http/PostsController.ts
...
public async destroy ({ params }: HttpContextContract) {
  const postId = Number(params.id) //transform to number
  PostsController.posts = PostsController.posts.filter(p => p.id !== postId)
}
Enter fullscreen mode Exit fullscreen mode

Shall we test it? Not yet. We need to register a new route for the post deletion.

// file: start/routes.ts

import Route from '@ioc:Adonis/Core/Route'

Route.get('posts', 'PostsController.index')

Route.post('posts', 'PostsController.store') 

Route.delete('posts/:id', 'PostsController.destroy') //new route
Enter fullscreen mode Exit fullscreen mode

Now we can send a DELETE request to the API to remove a post.

Image of Postman Deleting a Post

Finding a Post

Add a show method to the PostsController. To present a post we
will send a GET request to /posts/:id passing the post id as the parameter. Well, we know already how to do that.

// file: app/Controllers/Http/PostsController.ts
...
public async show ({ params }: HttpContextContract) {
  const postId = Number(params.id)
  return PostsController.posts.find(p => p.id === postId)
}
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward, right? Let's improve it a bit. AdonisJS has many conventions and one of them is related to the routes.

Route resources

AdonisJS provides a shortcut to define all the RESTful routes using Route resources.

If we implement all the CRUD (create, read, update, delete) operations we would have a mapping for each of them and
if you use the Adonis Template Engine you would have more to navigate between the pages. To simplify this we can
use the Route.resource() method. Replace your routes.ts with the code below.

// file: start/routes.ts

import Route from '@ioc:Adonis/Core/Route'

Route.resource('posts', 'PostsController')
Enter fullscreen mode Exit fullscreen mode

The code above register the following routes along with the appropriate controller methods.

Image of AdonisJS CRUD Routes

However, when creating an API, we don't need routes to display any page like create and edit.
We can remove them using the apiOnly() method.

// file: start/routes.ts

import Route from '@ioc:Adonis/Core/Route'

Route.resource('posts', 'PostsController').apiOnly()
Enter fullscreen mode Exit fullscreen mode

Our current routes now are the following.

Image of AdonisJS CRUD API Only Routes

We still have the update method registered for PUT and PATCH, but we haven't implemented it (homework?), you can remove it
changing the routes.ts code to the below.

// file: start/routes.ts

import Route from '@ioc:Adonis/Core/Route'

Route.resource('posts', 'PostsController')
  .except(['update'])
  .apiOnly()
Enter fullscreen mode Exit fullscreen mode

Great! You may be wondering how about using a database? Every time we restart the server the posts are reverted
to the original values. Let's take this next step, stay with me. :)

Adonis Database ORM

AdonisJS 5 has first class support for SQL databases. The Database layer of the framework (Lucid) comes with versatile
set of tools, enabling us to build data driven applications quickly and easily. Lucid comes with an implementation
of the Active record pattern which supports the main relational databases.

Setup Lucid

To install and initialize Lucid, run the commands below.

$ yarn add @adonisjs/lucid@alpha

$ node ace invoke @adonisjs/lucid

The command above will create the default config file and register @adonisjs/lucid under the providers array. We
are going to use SQLite, you can install it with the command below.

$ yarn -D add sqlite3

On the config/database.ts you can see the configuration options. There are examples for many
databases but what defines the chosen option is your .env file. Make sure it has the property
DB_CONNECTION=sqlite.

The database file lives inside the tmp path. So, create the tmp directory inside the project root.

$ mkdir tmp

Creating the Post model

To make sure your commands are up to date, build the project with yarn build.
Next, run the following command to create your first data model.

$ node ace make:model Post

It will create a new model in app/Models directory with the following content.

// file: app/Models/Post.ts

import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'

export default class Post extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}
Enter fullscreen mode Exit fullscreen mode

This is a boilerplate class, you can remove the properties if you need it. In our case we will keep it, and we
will just add two new properties for title and content.

// file: app/Models/Post.ts

import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'

export default class Post extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public title: string

  @column()
  public content: string

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}
Enter fullscreen mode Exit fullscreen mode

We created the post model and following Adonis convention it maps automatically to a database table called posts.
This table is not created atomatically by Adonis, we need to use the migrations for that.

Schema Migrations

Schema migrations offer a robust API for evolving and tracking database changes. You can create/modify database
by just writing Javascript/TypeScript.

Let's execute the following command to create a new migration file.

$ node ace make:migration posts

This creates a file in database/migrations, in my case called 1594401640375_posts.ts. The file will have the
boilerplate with the default columns.

 // file: database/migrations/1594401640375_posts.ts

 import BaseSchema from '@ioc:Adonis/Lucid/Schema'

 export default class Posts extends BaseSchema {
   protected tableName = 'posts'

   public async up () {
     this.schema.createTable(this.tableName, (table) => {
       table.increments('id')
       table.timestamps(true)
     })
   }

   public async down () {
     this.schema.dropTable(this.tableName)
   }
 }
Enter fullscreen mode Exit fullscreen mode

Next, we add the columns for title and content. As far as I know, AdonisJS doesn't sync your model with the
migrations file (I think Rails does that), we need to add them manually. So, let's do it.

// file: database/migrations/1594401640375_posts.ts

import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class Posts extends BaseSchema {
  protected tableName = 'posts'

  public async up () {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string('title').notNullable()
      table.string('content').notNullable()
      table.timestamps(true)
    })
  }

  public async down () {
    this.schema.dropTable(this.tableName)
  }
}
Enter fullscreen mode Exit fullscreen mode

With the migration code completed let's build the app and apply it. Run the command below.

$ yarn build && node ace migration:run

If you re-run the same command, Lucid will show that migrations are up to date. All right!
Now we can change our PostsController to use the Post model.

Using the Post model on the Controller

The project is using the boring in memory array to persist data. We just created our post model using Lucid and
we setup SQLite database so, let's use it!

Change the PostsController store method to the code below.

// file: app/Controllers/Http/PostsController.ts
...
public async store ({ request }: HttpContextContract) {
    const data = request.only(['title', 'content'])
    const post = {
      title: data.title,
      content: data.content,
    }
    return await Post.create(post)
}
Enter fullscreen mode Exit fullscreen mode

We removed the line we were creating the Id and instead of pushing the new post to the array we create it using
the Post.create() method. You can test it using the same requests we used before with Postman. With this working
fine we should be able to list all the posts.
Let's change the index and show methods to fetch posts from the database.

// file: app/Controllers/Http/PostsController.ts
...
public async index () {
  return await Post.all()
}

public async show ({ params }: HttpContextContract) {
    return await Post.find(params.id)
}
Enter fullscreen mode Exit fullscreen mode

The last remaining is the destroy method. We need to find the post, and then we can delete it.

// file: app/Controllers/Http/PostsController.ts
...
public async destroy ({ params }: HttpContextContract) {
    const post = await Post.find(params.id)
    post?.delete()
}
Enter fullscreen mode Exit fullscreen mode

We are done! You can find the complete project on my Github.
https://github.com/fredmaiaarantes/blog-api-adonisjs-5

We have a lot more to explore about AdonisJS 5: validations, model relations, query builder, seeds, tests,
authentication, and so son. Let me know if you want to hear more.

Do you have any feedback or suggestion? Leave a polite comment below. :)

Discussion (3)

pic
Editor guide
Collapse
mortenko profile image
mortenko

I was expecting some example with hardcore typescript typing but little bit disappointed. Can you maybe next type provide an example how to correctly type adonis stuff ?

Collapse
fredmaiaarantes profile image
Frederico Maia Arantes Author

Hi @mortenko , thanks for the comment.
All the code code I made is using TypeScript and the correct AdonisJS types. Do you mean more examples of model classes? I think I didn't get it. What do you mean by "hardcore typescript typing"?

Collapse
mortenko profile image
mortenko

I mean typing classes, variables, functions, models, migrations... everything:) As I remember, doc for adonis v5 is quite poor. They are expecting to use TS but did not provide any examples.