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.
To follow along with this tutorial, you’ll need:
To build the backend, complete the following steps.
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.
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 databody-parser
— a pachage that allows Express to read the request body and then parse that into a JSON object that can be understoodcors
— a middleware that can be used to enable CORS with various optionsmorgan
— a logging tool that logs all routes and requestsaws-sdk
— the official AWS SDK for JavaScript, available for browser, mobile devices, and Node.js backendsmulter
— a Node.js middleware for handling multipart form data, which is primarily used for uploading filesdotenv
— 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 S3nodemon
a package that reruns the Express server every time we make changes to our codeStart 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.
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:
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.
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.
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.
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.
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.”
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.
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.
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.
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:
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; }
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 ]
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.
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.
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.
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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.