Traavel: plan your next trip in a few clicks!
Dive into the making of this NextJS web application powered by Amplify
✨ Introduction
Planning a trip? Picture this - no more chaos, no need to juggle tabs or scan endless reviews. Just a few clicks, and you're set. Welcome to Traavel! It's the travel planner that's all about simplicity and convenience. And in this article, we're going behind the scenes.
We'll take a close look at how Traavel is built, from its foundations in NextJS to the powerful engine running it all, AWS Amplify. It's a journey through the lines of code and technologies that power your travels. So, if you're ready, let's hit the road!
⚡️ Main features
Let's take a look at the main features of the application. We'll dig deeper into the technical details later.
👤 Authentication
We understand the importance of security 🔐. That's why Traavel includes a user authentication feature, powered by Cognito, to keep your travel plans and personal information safe.
Using Amplify, this is all you need in terms of code to have a nice signin / signup / forgot-password modal page:
<Authenticator variation="modal" />
⛰️ Trips management
With Traavel, creating, updating and deleting trips is as easy as pie 🥧. Just select the destination, the start and end dates of your trip, and that's it! You can start adding activities to your days, splitting our itinerary into manageable, enjoyable chunks.
The destination input is powered by Google Places API so that you can get suggestions of places in real time.
After creating the trip, Traavel automatically sets a representative image of the city you're traveling to. This is making use of the Unsplash API, and of course you can update this image at any time.
🌍 Interactive map
What would a travel application be without an interactive map 🗺️? The answer - not quite as powerful. That's why we've equipped Traavel with a dynamic, user-friendly interactive map, thanks to the Google Maps API.
This map doesn't just pinpoint locations; it breathes life into your itinerary, providing real-time directions and laying out your activities for each day. So, whether you're navigating the maze-like streets of Venice or hunting down a hidden gem of a restaurant in New York, Traavel's interactive map makes sure you're never lost.
🚀 Try it!
And that's the gist of it! I invite you to try Traavel at this URL: https://main.d1q4622p31lt9z.amplifyapp.com. I'm sure you'll not be disappointed 😆 (and if you are, leave a comment telling me why 🥲).
You can use the following demo credentials if you don't want to register (although I recommend creating a new user):
Username: test@traavel.com
Password: traavel
📹 Demo video
🛠️ Tech stack
Ok now it's time for the good stuff, I know you were waiting for it 😏. Let's break Traavel down piece by piece.
🌐 Frontend
Built on NextJS, a powerful React-based framework, our frontend forms the user interface of Traavel. This is what our users will be interacting with, where trips are planned, itineraries organized, and maps come alive.
NextJS is the star of the show here, providing the flexibility and modularity of React while offering the benefits of server-side rendering for speedy page loads.
Furthermore, NextJS integrates seamlessly with AWS Amplify, creating a smooth developer experience that's hard to beat.
⚙️ Backend
Here is where all the magic happens 🪄. The backend is the invisible power force that propels Traavel, ensuring every feature, every function, works flawlessly. Entirely developed using AWS Amplify, it provides the robustness and scalability that our application needs to serve users around the globe.
Amplify, with its various components, handles a multitude of tasks. It deals with authentication via Amazon Cognito, manages our application's database using DynamoDB, and provides an easy-to-use GraphQL API with AWS AppSync.
Cognito
Cognito powers the user authentication process on Traavel. Whether a user is signing up for the first time or logging back in to tweak their trip plans, Cognito ensures these processes are secure. It provides user management and identity services that are easy to set up and use, relieving us from the heavy lifting of building and maintaining a user authentication system.
Just use the Amplify CLI or Amplify Studio (if you prefer to do it through the UI) to set up the auth configuration. I personally choose the command line interface, it makes me look cooler 😎:
amplify add auth
You'll be asked a series of questions to configure your authentication service. You can choose the default configuration with minimal setup or manually configure settings to better fit your needs. Want to provide sign in with a username, email, or phone number? No problem. Want to include MFA (Multi-Factor Authentication) or add social providers like Google or Facebook? Cognito has got you covered.
Once you're done with the setup, simply push your changes with amplify push
and voilà! Your authentication service is ready to protect your users and their data.
AppSync and DynamoDB
AppSync and DynamoDB: two key players in the Traavel team. AppSync handles the GraphQL API, enabling efficient data transfers and ensuring our users can plan their trips. DynamoDB, our chosen NoSQL database, stores all our data, keeping everything swift and scalable.
Simply put, AppSync gets and sends the data, DynamoDB stores it. Together, they ensure smooth and reliable data management for Traavel, allowing for a seamless user experience.
Just run:
amplify add api
Choose the GraphQL API, define your data model in your schema.graphql file and run amplify push
again. That's it. DynamoDB will create the proper tables for you and AppSync will create the queries, mutations and subscriptions needed to interact with the data.
The most difficult thing about this is defining the data model, but that's a different article 😜.
I leave here the GraphQL schema in case anyone is interested:
enum ActivityType {
FLIGHT
HOTEL
MUSEUM
VISIT
RESTAURANT
BAR
CONCERT
MEETING
THEATER
CRUISE
OTHER
}
type Activity @model @auth(rules: [{ allow: public }]) {
id: ID! @primaryKey
dayId: ID! @index(name: "byDay", sortKeyFields: ["startTime"])
startTime: AWSDateTime!
endTime: AWSDateTime!
name: String!
description: String
location: Location!
type: ActivityType!
}
type Day @model @auth(rules: [{ allow: public }]) {
id: ID! @primaryKey
tripId: ID! @index(name: "byTrip", sortKeyFields: ["date"])
date: AWSDateTime!
activities: [Activity] @hasMany
}
type Location {
latitude: Float
longitude: Float
}
type User @model @auth(rules: [{ allow: public }]) {
id: ID! @primaryKey
email: AWSEmail!
name: String
avatarUrl: AWSURL
trips: [Trip] @hasMany
}
type Trip @model @auth(rules: [{ allow: public }]) {
id: ID! @primaryKey
userId: ID! @index(name: "byUser", sortKeyFields: ["startDate"])
name: String!
destination: String!
location: Location!
startDate: AWSDateTime!
endDate: AWSDateTime!
imgUrl: String
days: [Day] @hasMany
}
Lambda functions
In Traavel, we've utilized two Lambda functions to handle specific tasks:
- User Post-Confirmation Trigger: with Cognito, you can attach lambda functions that will run on certain events, such as user sign-up, confirmation or sign-in. In my case, I wanted to create a user in the "User" table. So I ran
amplify add function
and pasted the following code to implement this functionality:
/* Amplify Params - DO NOT EDIT
API_TRAAVELWEBAPP_GRAPHQLAPIIDOUTPUT
API_TRAAVELWEBAPP_USERTABLE_ARN
API_TRAAVELWEBAPP_USERTABLE_NAME
ENV
REGION
Amplify Params - DO NOT EDIT */
const AWS = require("aws-sdk");
const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" });
/**
* @type {import('@types/aws-lambda').APIGatewayProxyHandler}
*/
exports.handler = async (event) => {
const date = new Date();
if (event.request.userAttributes.sub) {
let params = {
Item: {
id: { S: event.request.userAttributes.sub },
__typename: { S: "User" },
name: { S: event.request.userAttributes.name },
email: { S: event.request.userAttributes.email },
createdAt: { S: date.toISOString() },
updatedAt: { S: date.toISOString() },
_version: { N: "1" },
_lastChangedAt: { N: Date.now().toString() },
},
TableName: process.env.API_TRAAVELWEBAPP_USERTABLE_NAME,
};
// Call DynamoDB
try {
await ddb.putItem(params).promise();
console.log("Success: Everything executed correctly");
} catch (err) {
console.log("Error", err);
}
return event;
} else {
// Nothing to do, the user's email ID is unknown
console.log("Error: Nothing was written to DynamoDB");
return event;
}
};
Amplify makes it easy to integrate your lambda function with the DynamoDB table using environment variables such as API_TRAAVELWEBAPP_USERTABLE_NAME
. After that, update your Cognito configuration to assign this trigger by running amplify update auth
. Finally, run amplify push
again to deploy the changes.
- Trip deletion: the Traavel data model is something like this -> one user has many trips; one trip has many days; and one day has many activities. In order to completely delete a trip, we also want to remove all those associated identities. This means that several mutations must be made, and I don't want the user to be waiting for all that logic to be finished so that it can move on. So, my solution was to leverage all that logic to a lambda function that will be executed asynchronously while the user keeps using the app:
/* Amplify Params - DO NOT EDIT
API_TRAAVELWEBAPP_ACTIVITYTABLE_ARN
API_TRAAVELWEBAPP_ACTIVITYTABLE_NAME
API_TRAAVELWEBAPP_DAYTABLE_ARN
API_TRAAVELWEBAPP_DAYTABLE_NAME
API_TRAAVELWEBAPP_GRAPHQLAPIIDOUTPUT
API_TRAAVELWEBAPP_TRIPTABLE_ARN
API_TRAAVELWEBAPP_TRIPTABLE_NAME
ENV
REGION
Amplify Params - DO NOT EDIT */
const AWS = require("aws-sdk");
const client = new AWS.DynamoDB.DocumentClient();
/**
* @type {import('@types/aws-lambda').APIGatewayProxyHandler}
*/
exports.handler = async (event) => {
console.log(`EVENT: ${JSON.stringify(event)}`);
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "DELETE, OPTIONS",
};
if (event.httpMethod !== "DELETE") {
return {
statusCode: 400,
headers,
body: "Only DELETE method is allowed",
};
}
try {
// Fetch the tripId from event query params.
const { tripId } = event.queryStringParameters;
// Query to get all Days associated with the Trip.
const queryDaysParams = {
TableName: process.env.API_TRAAVELWEBAPP_DAYTABLE_NAME,
IndexName: "byTrip",
KeyConditionExpression: "tripId = :tripId",
ExpressionAttributeValues: { ":tripId": tripId },
ProjectionExpression: "id",
};
const { Items: days } = await client.query(queryDaysParams).promise();
console.log(`DAYS: ${JSON.stringify(days)}`);
// For each Day, get all associated Activities
const activityDeletionPromises = [];
const dayDeletionPromises = [];
for (const day of days) {
const queryActivitiesParams = {
TableName: process.env.API_TRAAVELWEBAPP_ACTIVITYTABLE_NAME,
IndexName: "byDay",
KeyConditionExpression: "dayId = :dayId",
ExpressionAttributeValues: { ":dayId": day.id },
ProjectionExpression: "id",
};
const { Items: activities } = await client
.query(queryActivitiesParams)
.promise();
console.log(`ACTIVITIES: ${JSON.stringify(activities)}`);
// Delete activities associated with this day
for (const activity of activities) {
const deleteActivityParams = {
TableName: process.env.API_TRAAVELWEBAPP_ACTIVITYTABLE_NAME,
Key: { id: activity.id },
};
activityDeletionPromises.push(
client.delete(deleteActivityParams).promise()
);
}
// Delete this day
const deleteDayParams = {
TableName: process.env.API_TRAAVELWEBAPP_DAYTABLE_NAME,
Key: { id: day.id },
};
dayDeletionPromises.push(client.delete(deleteDayParams).promise());
}
// Delete the Trip
const deleteTripParams = {
TableName: process.env.API_TRAAVELWEBAPP_TRIPTABLE_NAME,
Key: { id: tripId },
};
const tripDeletionPromises = [client.delete(deleteTripParams).promise()];
// Wait for all deletions to complete
await Promise.all([
...activityDeletionPromises,
...dayDeletionPromises,
...tripDeletionPromises,
]);
return { statusCode: 200, headers, body: "Successfully deleted item" };
} catch (error) {
console.log(`Error: ${error}`);
return { statusCode: 500, headers, error };
}
};
Amplify Hosting
I want to finally mention a feature that's as important as any other we've discussed: Amplify Hosting. This is what brings Traavel to the world, making our application accessible to travelers everywhere.
Amplify Hosting provides a fast, reliable, and secure environment for our NextJS application. It simplifies the deployment process with features like continuous deployment from your Git repository and custom domain setup (which I've not set up, that's why the URL is so ugly). With just a few clicks, our updates are live and ready for users to enjoy.
😥 Limitations and challenges faced
Ok, let's talk now about the bad things. I'll be honest: implementing Traavel has not been a bed of roses. As with any other tool out there, Amplify has its pros and cons, and I've been struggling at some points of the development process trying to overcome those obstacles.
Storage files without expiration date
One feature that I finally didn't implement is allowing the user to upload their own pictures of the cities they're going to visit. For this, I needed to use the Storage component of Amplify, which sets up an S3 bucket for your users to store files.
Alright, I found out that currently Amplify doesn't allow retrieving those objects without an expiration time. This is, the URL returned by S3 will expire sooner than later, I think the maximum time allowed is 2 weeks. This means that, if a user chose to upload a photo for their trip, in two weeks the URL will be no longer valid, and a new one should be created. As you can imagine, this isn't an ideal scenario for a feature that's supposed to provide a seamless user experience.
It seems that the Amplify team is working on this feature already: https://github.com/aws-amplify/amplify-js/issues/9418, but at the moment we will have to make do with the images that Unsplash offers us.
Cache Unsplash photos URLs
Amplify has a basic cache system that leverages the browser's local storage. I thought this could be useful to store the response from Unsplash so that I don't throttle the API (or raise my bill sky-high).
The problem is that this Amplify caching system does some weird things. When I was switching between trips, sometimes the cache items were being overwritten. Seems like an issue with the Cache component, so I ended up manipulating the local storage directly instead of through Amplify.
DynamoDB batch operations
In a previous section, I talked about the need for deleting the trip and associated days and activities. This could be made more efficient if we leveraged DynamoDB batch operations.
The thing is that AppSync doesn't allow this by default. It can be implemented by defining your own custom mutation, I found this post which does exactly that: https://medium.com/@jan.hesters/creating-graphql-batch-operations-for-aws-amplify-with-appsync-and-cognito-ecee6938e8ee.
But I didn't want to spend so much time on that, so I finally ended up using for loops in my lambda function. Anyway, this process is invisible to the user, so I don't mind being a little inefficient here.
🤔 Conclusion
Embarking on our journey with Amplify to build Traavel has been quite an adventure. As with any travel experience, there have been scenic routes and a few detours. Through it all, it's evident that Amplify is a powerful tool that has enabled us to create an engaging and functional travel planning application.
It simplifies the backend creation process, but this simplicity can become a limitation when trying to implement more complex backend logic. Certain customizations can require a deeper understanding of AWS services than initially expected, and that might involve some head-scratching moments.
Also, as Amplify is a high-level service, it abstracts away many of the nitty-gritty details of underlying services. While this is often a plus, it can sometimes become a hurdle, especially when debugging or trying to perform more specific tasks that require direct interaction with these underlying services.
My experience suggests that Amplify is an excellent choice for simple applications or projects where its built-in functionalities closely align with your needs. For small to medium-sized projects, the time and effort Amplify saves can be a game-changer.
However, for more complex or specialized use cases, the limitations of Amplify might start to surface. You might find yourself spending more time than you'd like working around these constraints or diving deeper into AWS services to get things working.
In these cases, it might be worth considering other options or being prepared to build custom solutions. Yet, even with the bumps in the road, our journey with Amplify has been a fruitful one, and the resulting application, Traavel, has made it all worth it 😃.