Wisdom Ekpot A student of Ibom Metropolitan Polytechnic studying computer engineering, Wisdom has been writing JavaScript for two years, focusing on Vue.js, Angular, and Express.js.

Build an event keeper app with Ionic and AWS

12 min read 3594

Build an Event Keeper App With Ionic and AWS

The Ionic framework is an open-source UI toolkit for building performant, high-quality mobile and desktop apps using web technologies (HTML, CSS, and JavaScript) with integrations for popular frameworks such as Angular, Vue, and React.

Amazon Web Services (AWS) provides on-demand cloud platforms on a pay-as-you-go basis.

In this tutorial, we’ll build an event keeper app using an AWS service called S3 to save our images. All images uploaded to our platform will be stored in the AWS S3 bucket and the URL of our image will be returned.

Prerequisites

To follow along with this tutorial, you’ll need:

  • A basic understanding of HTML, CSS, and JavaScript (ES6+)
  • A code editor, such as VS Code, installed
  • POSTMAN installed
  • MongoDB set up
  • Basic knowledge of Angular and Express.js

Building the backend

To build the backend, complete the following steps.

  1. Initialize a new Node project
  2. Install all the required dependencies
  3. Create an Express server and configure the dependencies
  4. Create and configure MongoDB
  5. Create the event keeper model
  6. Set up AWS and Multer
  7. Create the necessary routes and controllers
  8. Test the routes using POSTMAN

Initialize a new Node project

Let’s start by creating a folder on the desktop for our application. Open up your terminal and type the following.

cd desktop
mkdir event-app && cd event-app
mkdir server && cd server
code

Here we’re creating an event-app directory on the desktop. Inside the event-app directory, we’ll create a server directory. This is where we’ll set up our backend application.

The code . command will set up our application in VS Code. VS Code is redefined and optimized for building and debugging modern web and cloud applications. Feel free to use any text editor of your choice. We’ll use the VS Code integrated terminal for the purpose of this tutorial.

To set up a Node.js application, we’ll need a package.json file to manage all our project dependencies. To create that, run npm init -y on the terminal.

npm init -y

This will create a package.json file.

We made a custom demo for .
No really. Click here to check it out.

Install required dependencies

Our application will need the following npm packages for development.

  • Expressjs — a node.js framework that makes it easy to build web applications.
  • mongoose — an object modeling tool used to asynchronously query MongoDB that provides a straightforward, schema-based solution to model your application data
  • body-parser — a pachage that allows Express to read the request body and then parse that into a JSON object that can be understood
  • cors — a middleware that can be used to enable CORS with various options
  • morgan — a logging tool that logs all routes and requests
  • aws-sdk — the official AWS SDK for JavaScript, available for browser, mobile devices, and Node.js backends
  • multer — a Node.js middleware for handling multipart form data, which is primarily used for uploading files
  • dotenv — a zero-dependency module that loads environment variables from an .env file into process.env. This is where we’ll store all outher environment variables for our application, such as AWS secret, ID, etc.
  • multer-s3 — a package used for streaming the Multer storage engine for AWS S3
  • nodemon a package that reruns the Express server every time we make changes to our code

Start by installing Nodemon globally so that it can be accessed anywhere on your local machine.

npm install -g nodemon

Next, install all the other dependencies.

npm i --save express body-parser cors morgan aws-sdk multer multer-s3 mongoose dotenv

This command will install all these packages and add them to our dependencies in the package.js file. It also creates a node-modules directory, this is where all our packages are being stored.

Remember not to add this package while you’re pushing your project to git. To avoid this, create a .gitignore file

touch .gitignore

This command will create a .gitignore file. Open the file and type node-modules in it.

Create an express server and configure all dependencies.

To set up the Express server, create a src directory and place an index.js file inside it. This is the base file for our application. We’ll define our first route here and configure some packages.

Add the following code to the index.js file.

require('dotenv').config()
const express = require("express");
const PORT = process.env.PORT || 4000;
const morgan = require("morgan");
const cors = require("cors");
const bodyParser = require("body-parser");
const app = express();
//--------------registering cors -----------------
app.use(cors());
//----------------configure body parser --------------
app.use(bodyParser.urlencoded({
    extended: false
}));
app.use(bodyParser.json());
//---------------configure morgan  --------
app.use(morgan("dev"));

// define first route
app.get("/", (req, res) => {
    res.json({
        data: "Hello MEIN developers...The `I` stands for Ionic😎😎"
    });
});
app.listen(PORT, () => {
    console.log(`App is running on ${PORT}`);
});

Open the package.json file and add the following. Running npm run dev:start will bootstrap our application.

"scripts": {
    "start:dev":"nodemon src"
  },

If you open up the application on the browser, you should see this:

Open App in Browser

Create and configure the MongoDB database

Create a config directory. Inside the config directory, create a mongoose.js file and add the following code to the file.

const mongoose = require("mongoose");
module.exports = function (app) {
    mongoose.connect("mongodb://localhost:27017/ionic-app", {
        useUnifiedTopology: true,
        useNewUrlParser: true,
        useFindAndModify: false
    }).then(res => console.log("Database conneceted")).catch(err => console.log(err))
    mongoose.Promise = global.Promise;
    process.on("SIGINT", cleanup);
    process.on("SIGTERM", cleanup);
    process.on("SIGHUP", cleanup);
    if (app) {
        app.set("mongoose", mongoose);
    }
};
function cleanup() {
    mongoose.connection.close(function () {
        process.exit(0);
    });
}

We need to register it in our root file and then pass the instance of express(). To do this, we need to require this module in our index.js file.

require("./config/mongoose")(app);

Once you do this, you’ll see Database conneceted logged on the console if your application is running.

Create the event keeper model

Now that our database is ready, we need to create our Mongoose schema, which takes in an object that defines the property of the event schema.

Create a directory to hold the schema, routes, and controllers.

cd src
mkdir api && cd api
mkdir events && cd events
touch model.js && touch controller.js && touch routes.js

Lets define our events schema by add this codes in our events/model.js file:

const mongoose = require("mongoose");
const eventSchema = mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    image: {
        type: String,
        required: true
    },
    description: {
        type: String,
        required: true
    }
}, {
    timestamps: true,
});
const Event = mongoose.model("Event", eventSchema);
module.exports = Event;

Mongoose converts the schema into a document in the database. Those properties are then converted into fields in the document.

Set up AWS and Multer

First, you’ll need to register for an AWS account.

Once you log in, you’ll see services link on the navigation bar. Click on it and type “s3” into the search box.

AWS Services Search Page

Click on S3. This is what we’ll use for our cloud storage.

Create a new bucket for your application by clicking “create bucket.” A modal will pop up asking you to specify the name of your bucket. Call your bucket eventskeeper and choose US West (N. California) as your region.

Go to the “Permissions” section and uncheck the checkbox to block all public access, then save your changes.

AWS Services Permissions Tab

Nex, we need to get all our security credentials. Click on the logged-in user’s name at the top of the page and then click “My Security Credentials.”

AWS Management Console

This will take you to a page where you can set your security credentials. Click “Access Keys” and then “Create New Access Keys.” You’ll see a modal that contains your security details. Copy these details so you can store them in an .env file.

Create the .env file.

touch .env

Inside the .env file, add the following.

ACCESS_KEY=****************ZZN7SQDUJUPQ
ACEESS_SECRET=**********************ifn30**

Replace the values with the security details you got from the site. Don’t forget to add this file to the .gitignore file.

Create a aws.js file inside your config directory and add the following.

const aws = require('aws-sdk')
const multer = require('multer')
const multerS3 = require('multer-s3')


aws.config.update({
    secretAccessKey: process.env.ACEESS_SECRET,
    accessKeyId: process.env.ACCESS_KEY,
    region: 'eu-west-3'
})
const s3 = new aws.S3();
const fileFilter = (req, file, cb) => {
    if (file.mimetype.startsWith('image')) {
        cb(null, true);
    } else {
        cb(new Error('Not an image! Please upload an image.', 400), false);
    }
};
const upload = multer({
    fileFilter,
    storage: multerS3({
        s3,
        bucket: 'eventskeeper',
        acl: 'public-read',
        metadata: function (req, file, cb) {
            cb(null, {
                fieldName: "TESTING_META_DATA"
            });
        },
        key: function (req, file, cb) {
            cb(null, Date.now().toString())
        }
    })
})
module.exports = {
    upload,
    s3
}

Start by importing aws-sdk,multer, and multer-s3 into this file. Next, use the credentials stored in the environment variable to configure your aws-sdk package. Replace your region with the region in the config object.

The next step is to create a simple filter for your file upload that will only accept file types in which the mimetype starts from image.

After defining this filter, create a Multer instance to take in the filter and the bucket config to which we’ll be sending our files. In the multerS3 instance, specify the name of the bucket that will hold your file, then set alc to public read so that you can add files at any time.

To use these methods, we need to export the functions so that it can be accessible by any JS module.

Create the necessary routes and controllers.

We basically have two routes: one to get all the events and the other to add a new event.

In the api/events/routes.js file, add the following routes.

const eventController = require("./controller");
const router = require("express").Router();

router.post("/", eventController.addEvent)
router.get("/", eventController.getAllEvents)


module.exports = router;

We have to create the addEvent and getAllEvents methods in our controller file. Start by creating the addEvent method in the api/events/controller.js file.

let Event = require('./model');
const {
    upload,
    s3
} = require('../../config/aws')

const singleUpload = upload.single('image');
exports.addEvent = async (req, res) => {
    try {
        await singleUpload(req, res, err => {
            if (err) {
                return res.status(422).json({
                    title: "File Upload Error",
                    desription: err.message
                })
            }
            const event = new Event({
                name: req.body.name,
                description: req.body.description,
                image: req.file.location
            })
            event.save();
            res.status(200).json({
                status: "Success",
                mes: "New Event Created"
            })
        })
    } catch (err) {
        res.status(500).json({
            status: "error",
            error: err
        })
    }
}

We also need to bring in our event module and the AWS config module. Start by checking whether there are any errors while trying to upload the image, and throw a 422 error for any unprocessable entity. If there are no errors, use the req.body to access the JSON data that was sent in the request and use the req.file.location to get the URL of the image.

Next, create the getAllEvents function. This will basically get all the events stored in MongoDB.

exports.getAllEvents = async (req, res) => {
    try {
        let events = await Event.find();
        res.status(200).json({
            status: "Success",
            data: events
        })
    } catch (err) {
        res.status(500).json({
            status: "error",
            error: err
        })
    }
}

Use the find() method to fetch all data stored in the DB.

Now we need to register a global route handler for our events. Create a routeHandler.js file inside the api directory and add the following code.

const EventRoutes = require("./events/routes");
module.exports = (app) => {
    app.use("/event", EventRoutes);
}

Next, register it in the index.js file.

require("./api/routehandler")(app);

It’s time to test our application on POSTMAN.

Posting an event

Post an Event

Getting all events

Get All Events

Setting Up the Mobile App

Install Ionic and other Ionic tools globally.

npm install -g @ionic/cli native-run cordova-res

Create a new directory called events on your desktop. Init a new Ionic application and Angular inside that directory.

cd desktop
cd event-app && mkdir events && cd events
ionic start events tabs --type=angular --capacitor

This Ionic Angular app uses the Tabs starter template and adds Capacitor for native functionality.

Now we can move into our application by running:

cd events

The Ionic PWA elements library will provide the web-based functionality in our application. To install it, run the following command inside your application directory.

npm install @ionic/pwa-elements

To use this package, we need to import it into our src/main.ts file.

import { defineCustomElements } from '@ionic/pwa-elements/loader';

// Call the element loader after the platform has been bootstrapped
defineCustomElements(window);

If you dont have Ionic lab already installed on your application, you can run npm i -D -E @ionic/lab on your terminal to install it.

ionic serve --lab

This will open up our application port 8100 in our browser.

App Open on Port 8100

Setting up the user interface

It’s time to set up the user interface. We’ll start by customizing the tabs.

Head over to src/app/tabs/tabs.page and modify the code there like so:

<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="tab1">
      <ion-icon name="home-outline"></ion-icon>
      <ion-label>Events</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="tab2">
      <ion-icon name="add-outline"></ion-icon>
      <ion-label>Add Event</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

We’ll use cards to represent our events, so now head over to src/app/tab1/tab1.page.html and modify the code there with the following.

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Events
    </ion-title>
  </ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
  <ion-card>
    <img style="width:100%;height:250px;object-fit: cover;"
      src="https://images.pexels.com/photos/3323208/pexels-photo-3323208.jpeg?cs=srgb&dl=woman-leaning-on-sinalco-cola-can-3323208.jpg&fm=jpg"
      alt="">
    <ion-card-header>
      <ion-card-subtitle>Card Subtitle</ion-card-subtitle>
      <ion-card-title>Card Title</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      Keep close to Nature's heart... and break clear away, once in awhile,
      and climb a mountain or spend a week in the woods. Wash your spirit clean.
    </ion-card-content>
  </ion-card>
</ion-content>

The result of the modification should look like this:

Modified Code Result

Create a form in the tabs2 component for adding a new event. Add the following to src/app/tab2/tab2.page.html.

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Add Event
    </ion-title>
  </ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true" class="ion-padding">
  <div id="container">
    <form>
      <ion-item id="rounded">
        <ion-label position="stacked">Name</ion-label>
        <ion-input id="input"></ion-input>
      </ion-item>
      <ion-item id="rounded">
        <ion-input id="photo" name="file" type="file" accept="image/*" (change)="handleFileInput($event.target.files)">
        </ion-input>
      </ion-item>
      <ion-item>
        <ion-label position="stacked">Description</ion-label>
        <ion-textarea rows="6" cols="20"></ion-textarea>
      </ion-item>
      <div class="ion-padding">
        <ion-button expand="block" type="submit" class="ion-no-margin">Add An Event</ion-button>
      </div>
    </form>
  </div>


</ion-content>

Next, add some styles to src/app/tab2/tab2.page.scss.

#rounded {
    width: 100%;
    margin-top: 30px;
    -moz-border-radius: 50px;
    -webkit-border-radius: 50px;
    border-radius: 10px;
    margin-bottom: 10px;
}

Add Styles to Events

Creating the event service

The next step is to create a service to handle all the HTTP requests for our application.

ionic generate service api/event

Running this command will generate an api directory. Inside the api directory, modify the event.service.ts like so:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})

export class EventService {
  readonly rootURL: string = 'http://localhost:4000';
  getAllEvents() {
    return this.http.get(`${this.rootURL}/event`);
  }
    addNewEvent(payload) {
      return this.http.post(`${this.rootURL}/event`, payload);
    }
  constructor(private http: HttpClient) {}
}

Next, register Angular’s HttpClientModule in your src/app/app.module.ts.

import { HttpClientModule } from '@angular/common/http';

Then, add it to the imports array.

  imports: [  ...other packages, HttpClientModule ]

Implementing The APIs

We can now bring our services into our tab1 and tab2 components. Edit the code in src/app/tab1/tab1.page.ts with the following.

import { Component } from '@angular/core';
import { EventService } from '../../app/api/event.service';
@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: [ 'tab1.page.scss' ]
})
export class Tab1Page {
  constructor(private Event: EventService) {}
  events = [];
  getEvents() {
    console.log('hello');
    this.Event.getAllEvents().subscribe((data: any) => {
      this.events = data.data.reverse();
//the reverse method will get from the latest event added
    });
  }
  ngOnInit(): void {
    //Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    //Add 'implements OnInit' to the class.
    this.getEvents();
  }
}

Basically, what we’re doing here is bringing in our events service, then creating a state to hold all the events. We’re also creating a new method to query for all events and registering it in the ngOnInit lifecycle hook.

We’ll display our events on the card on our template by modifying the code.

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Events
    </ion-title>
  </ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
  <ion-card *ngFor="let event of events">
    <img style="width:100%;height:250px;object-fit: cover;" [src]="event.image" alt="">
    <ion-card-header>
      <ion-card-title>{{event.name}}</ion-card-title>
      <ion-card-subtitle>{{event.createdAt |date}}</ion-card-subtitle>
    </ion-card-header>
    <ion-card-content>
      {{event.description}}
    </ion-card-content>
  </ion-card>
</ion-content>

Doing this will display all the events in our database.

All Events in the Database

Implementing Add Events

In javascript we use formData to handle file uploads.We will use the angular reactive forms to handle the file uploads.We will start by importing FormsModule and ReactiveFormsModule modules in our src/app/tab2/tab2.module.ts file:

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
imports: [... other modules, FormsModule,ReactiveFormsModule, ],

We can now use Angular’s reactive forms in our application components. Import FormGroup , FormControl, and EventService into the tab2.page.ts file and define an instance of the form using the formControl module.

import { FormGroup, FormControl } from '@angular/forms';
import { EventService } from '../../app/api/event.service';


//this should be added inside the Tab2Page Class
eventForm = new FormGroup({
    name: new FormControl(''),
    description: new FormControl('')
  });
  constructor(private Event: EventService) {}

We need to define a method to handle our input field for file upload. This will be a change event that will watch when a file is selected. Your input for file upload should look like this:

 <ion-input id="photo" name="file" type="file" accept="image/*" (change)="handleFileInput($event.target.files)">
        </ion-input>

Next, create a state to hold the file and define the handleFileInput method.

  fileToUpload: File = null;
  handleFileInput(files: FileList) {
    this.fileToUpload = files.item(0);
    console.log(this.fileToUpload);
  }

Now if you select an image to upload it, will be logged to the console. Try it out!

Next, create a method to add a new event. Add the following method to the tab2.page.ts file.

    addEvent() {
        const formData: FormData = new FormData();
        formData.append('image', this.fileToUpload, this.fileToUpload.name);
        formData.append('name', this.eventForm.value.name);
        formData.append('description', this.eventForm.value.description);
        this.Event.addNewEvent(formData).subscribe(
            (data: any) => {
                console.log(data);
            },
            (err: HttpErrorResponse) => {
                console.log({ error: err });
            }
        );
    }

Use the form data to append the data to your input fields and then pass the formData in the addNewEvent service as a payload.

After doing this, modify your tab2 form to the following.

  <form [formGroup]="eventForm" (submit)="addEvent()">
      <ion-item id="rounded">
        <ion-label position="stacked">Name</ion-label>
        <ion-input formControlName="name" id="input" required></ion-input>
      </ion-item>
      <ion-item id="rounded">
        <ion-input id="photo" type="file" accept="image/*" (change)="handleFileInput($event.target.files)">
        </ion-input>
      </ion-item>
      <ion-item>
        <ion-label position="stacked">Description</ion-label>
        <ion-textarea formControlName="description" rows="6" cols="20" required></ion-textarea>
      </ion-item>
      <div class="ion-padding">
        <ion-button expand="block" type="submit" class="ion-no-margin">Add An Event</ion-button>
      </div>
    </form>

Now refresh your application and try adding a new event.

Adding spinners and toast

We need to add preloaders and a toast so we can show users whether the add event methods was successful or not. The toast will log both success and error messages.

Import LoadingController and ToastController and then register them in the constructors.

import { LoadingController } from '@ionic/angular';
import { ToastController } from '@ionic/angular';

constructor(
        private Event: EventService,
        public loadingController: LoadingController,
        public toastController: ToastController
    ) {}

We have to define a method to show and hide our loaders and also show our toasts.

    async presentLoading() {
        const loading = await this.loadingController.create({
            message: 'Please wait...',
            translucent: true
        });
        return await loading.present();
    }
    async presentToast(message) {
        const toast = await this.toastController.create({
            message: message,
            duration: 2000
        });
        return toast.present();
    }

We can now modify our addEvent method.

addEvent() {
        this.presentLoading();
        const formData: FormData = new FormData();
        formData.append('image', this.fileToUpload, this.fileToUpload.name);
        formData.append('name', this.eventForm.value.name);
        formData.append('description', this.eventForm.value.description);
        this.Event.addNewEvent(formData).subscribe(
            (data: any) => {
                this.loadingController.dismiss();
                this.eventForm.reset();
                this.presentToast('Event Added');
            },
            (err: HttpErrorResponse) => {
                this.loadingController.dismiss();
                this.presentToast('Something Went Wrong');
                console.log({ error: err });
            }
        );
    }

Congratulations! You just built a simple event app.

Conclusion

In this tutorial, we demonstrated how to build a simple application with Ionic. We could add better features to our app, such as user sign-in and sign-up. Understanding how both backend and mobile apps communicate will help you build larger, more complex and feature-rich applications.

For source code used in this tutorial, head to GitHub.

Wisdom Ekpot A student of Ibom Metropolitan Polytechnic studying computer engineering, Wisdom has been writing JavaScript for two years, focusing on Vue.js, Angular, and Express.js.

Leave a Reply