Codementor Events

Image/File Upload in Adonis.js with Cloudinary

Published Mar 17, 2020
Image/File Upload in Adonis.js with Cloudinary

So earlier this week at work, a colleague had a chat with me on putting something he had learnt to use (android related stuffs) but needed a small API to work with. My initial thought was doing it the Express.js way like I’m used to but then I remembered I had a little romance with Adonis.js sometimes in the past and so I opted for Adonis.
Few hours after, I realized I needed to work with image upload and as usual cloudinary to the rescue. Halfway through, I got stuck but couldn’t find much help so I decided to write this once I was done.
This post assumes you are already using Adonis (probably in love with it), so I won’t go into trying to preach Adonis to you (you can still check here for how to get started). In this post we will create a post with image attached and upload the image to cloudinary.
First, let’s create a new App with

adonis new PostsApp

Then cd into the project directory and run the command below

adonis serve --dev

If all went well, the app should be served on port 3333. Visiting http://127.0.0.1:3333, we should see the page below

1*GlAnvCTeQBDwKyQmqRVvHg.png

Great! So what next? Remember Adonis is an MVC framework for Node.js yeah? So let’s start putting components of our small app together. Let’s start with migrations. For this tutorial, we’ll be using the MySQL database. So let’s install mysql first by running

npm install mysql --save

Once installed, we need to make Adonis aware of our database settings. Open the .env file and update the items below setting the values to your own database credentials.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=adonis

Note that the default DB_CONNECTION in Adonis is sqlite. Be sure to change it to mysql as above.

Next, we create the migration file to hold our posts table. The posts table should contain id, title, body image_url and timestamps. Run the following command to create the migration.

adonis make:migration posts

When prompted, choose the create table option to create a new table and press Enter. The newly created migration file can be found in database/migrations directory. Since we won’t be needing the existing tables, let’s delete the two default migration files already created by Adonis — the users table and the tokens table. Update the posts migration to the one below

up () {
  this.create('posts', (table) => {
    table.increments();
    table.string('title');
    table.text('body');
    table.string('image_url').nullable();
    table.timestamps()
  })
}

With that, we can now run our migration with

adonis migration:run

Once the migration has successfully run, we can then create our Post model with

adonis make:model Post

The above creates our model in the app/Models directory

Now, lets update our route appropriately. The routes can be found in start/routes.js Then we can update the routes as shown below

const Route = use('Route');

Route.get('/', 'PostController.index');
Route.post('/', 'PostController.create');

Next, we create the PostController with

adonis make:controller Post

When prompted, select for Http Requestsand then press Enter. The controller can be found in the app/Controllers/Http directory. Now that we have our controller setup, let’s create the necessary methods. For now, our PostController should look like this

'use strict';

const Post = use('App/Models/Post');

class PostController {

  async index( {view}) {
    const posts = await Post.all();
    return view.render('index', {posts: posts.toJSON()})
  }

  async create( {request, response, view}) {
    
  }
}

module.exports = PostController;

Let’s create the user interface for our little app now. We will need two files to hold our views. One for the master layout and another to hold our posts. Let’s create these two files with

adonis make:view master

and

adonis make:view index

The views can be found in the resources/views directory. The master.edge file is the base layout and is extended by index.edge . The master layout contains the following

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Posts App</title>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  {{style('style.css')}}
  <link href="https://fonts.googleapis.com/css?family=Comfortaa" rel="stylesheet">
</head>
<body>
  <div class="container-fluid">
    <div class="wrapper">
      @!section('content')
    </div>
  </div>
</body>
</html>

In index.edge , the form is represented as a bootstrap modal as below

@layout('master')

@section('content')
<button type="button" class="btn btn-info btn-lg pull-right" data-toggle="modal" data-target="#myModal">Add New</button>

<div id="myModal" class="modal fade" role="dialog">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">&times;</button>
        <h4 class="modal-title">Add New Post</h4>
      </div>
      <div class="modal-body">
        <form action="{{route('post.create')}}" method="post" enctype="multipart/form-data">
          <div class="form-group">
            <label>Title</label>
            <input class="form-control" name="title" required>
          </div>
          <div class="form-group">
            <label>Body</label>
            <textarea class="form-control" rows="5" name="body"></textarea>
          </div>
          <div class="form-group">
            <label>Upload Image</label>
            <input type="file" name="image" class="form-control">
          </div>
          <div class="form-group text-center">
            <input type="submit" class="btn btn-primary">
          </div>
        </form>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>
@endsection

Don’t forget the enctype="multipart/form-data" added to the form since we are submitting a file to the server.

Next is to process the file on the server. Let’s install the cloudinary Node.js library by running

npm install cloudinary --save

Then, signup on cloudinary if you dont have an account yet or if you already have an account, copy your credentials fro your dashboard and paste in your .env as below

CLOUDINARY_API_KEY = 
CLOUDINARY_API_SECRET = 
CLOUDINARY_CLOUD_NAME = 

Next, create a CloudinaryService.js file in the app/Services directory to hold our cloudinary config. The CloudinaryService.js file should have the following in it

const cloudinary = require('cloudinary');
const Env = use('Env');

cloudinary.config({
  cloud_name: Env.get('CLOUDINARY_CLOUD_NAME'),
  api_key: Env.get('CLOUDINARY_API_KEY'),
  api_secret: Env.get('CLOUDINARY_API_SECRET')
});

module.exports = cloudinary;

If you try to submit the form now, you’ll most likely get a CSRF token mismatch error like below

1*FFzUF3Wa21fUyZbvaYSCkw.png

This is because the token checker does not work with multipart/form-data. The workaround for this is to go to config/shield.js and add the URL to the filterUris in the csrf key like below. This prevents the token middleware from acting on this route.

csrf: {
  enable: true,
  methods: ['POST', 'PUT', 'DELETE'],
  filterUris: ['/'],
  cookieOptions: {
    httpOnly: false,
    sameSite: true,
    path: '/',
    maxAge: 7200
  }
}

Next, let’s process the form on the backend by updating the create method in the PostController to have the snippet below

'use strict';

const Post = use('App/Models/Post');
const CloudinaryService = use('App/Services/CloudinaryService');

class PostController {

  async index( {view}) {
    const posts = await Post.all();
    return view.render('index', {posts: posts.toJSON()})
  }

  async create( {request, response, session}) {
    const {title, body} = request.all();
    const file = request.file('image');
    try {
      const cloudinaryResponse = await CloudinaryService.v2.uploader.upload(file.tmpPath, {folder: 'postsapp'});
      let post = new Post();
      post.title = title;
      post.body = body;
      post.image_url = cloudinaryResponse.secure_url;
      await post.save();
      session.flash({success: 'Successfully added post'});
      return response.redirect('back');
    } catch (e) {
      session.flash({error: 'Error Uploading Image'});
      return response.redirect('/')
    }
  }
}

module.exports = PostController;

Note the v2 and the {folder: 'postsapp'} added to the cloudinary service to make sure images are uploaded to the folder.

Finally, let’s update the index.edge to display the posts and notifications and do a little styling. The styling is in the public/style.css file.

<div class="notifications">
  @if(old('success'))
    <div class="alert alert-success">{{old('success')}}</div>
  @elseif(old('error'))
    <div class="alert alert-danger">{{old('error')}}</div>
  @endif
</div>

<div class="posts-wrapper">
  <div class="row">
    @each(post in posts)
      <div class="col-sm-4 col-md-4 col-xs-12 single-post-wrapper">
        <div class="card">
          <div class="title text-center">
            {{post.title}}
          </div><hr/>
          <div class="content">
            <div class="post-image">
              <img src="{{post.image_url}}" height="150px" width="100%">
            </div>
            <div class="post-body text-center">
              {{post.body}}
            </div>
          </div>
        </div>
      </div>
    @endeach
  </div>
</div>

Style.css

body, html {
  background: #efefef;
}

.wrapper {
  padding: 50px;
}

.card {
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  background: #ffffff;
  padding: 20px;
  transition: 0.3s;
}

.card .title {
  font-weight: bold;
}

.notifications {
  padding-top: 30px;
}

.posts-wrapper {
  padding-top: 20px;
}

.post-body {
  padding-top: 10px;
}

.single-post-wrapper {
  height: 350px;
}

Testing our little app, we should have something like what’s shown below

1*1F-5G91c-wQAcu1lRy6rTA.gif

And that’s it! We have our working app with image upload function.

Discover and read more posts from Abeeb Amoo
get started
post comments1Reply
Raphael Ogbonnaya
3 years ago

thank you for this