Build a Property Booking Website with Starlette, MongoDB, and Twilio
Rate this tutorial
As developers we often have the ability to choose our own tools to get the job done. If you're like me, any time a new framework builds up a reputation for high performance, you jump right in to see what the developer experience is like. In today's article, I want to look at building a web application with a Python ASGI framework called Starlette that promises blazing fast performance and developer productivity. We'll put Starlette to the test by integrating MongoDB, Stitch, and Twilio into the mix to build an AirBnB-type property booking application.
If you would like to follow along with this tutorial, you can get the code from the GitHub repo. Also, be sure to sign up for a free MongoDB Atlas account to make it easier to connect your MongoDB database. In this tutorial, we'll make use of a sample dataset that is provided by MongoDB Atlas.
Starlette is a Python ASGI framework that shines. It is a fairly new framework but has already garnered the attention of many Python developers for its high performance, support for WebSockets, GraphQL, impressive 100% test coverage, and zero hard dependencies.
The framework is very light and easy to work with. The really impressive thing about Starlette is that it can be treated either as either an entire framework, or just a toolkit, where components can be independently utilized in your existing framework or application. For this tutorial, we'll treat Starlette as a framework and build our entire application with Starlette. For this tutorial, I'll assume you are familiar with Python, but if you need a refresher, there's no better place than the official Python tutorial.
Today we are going to be building a rental reservation system, similar to AirBnB, that we'll conveniently name MongoBnB. Our application will display a list of featured properties, allow the user to sort by various parameters, view additional details of a selected property, and finally book and cancel reservations.
We have a lot to do today, so let's dive right into it!
As I mentioned at the beginning of this article, we are going to use a sample dataset provided by MongoDB Atlas. If you don't already have an account, sign up for free here, and create a new cluster. Once you have a cluster up and running, whether it'd be new or existing, load the sample dataset by clicking on the three dots button to see additional options, and then select "Load Sample Dataset."
The sample dataset provided by MongoDB Atlas will create a number of collections containing various datasets. Navigate to the Collections tab to see them all. All of the datasets will be created as separate databases prefixed with
sample_
and then the name of the dataset. It may take a few minutes for all of the databases to be imported, but the one we care about for our application is called sample_airbnb
. Open this database up and you'll see one collection called listingsAndReviews
. Click on it to see the data we will be working with.This collection will have over 5,000 documents, with each containing all the information we could ever want for a particular listing. For our application, we'll only work with a subset of the data, but you will have lots of existing data to expand on in case you wish to add additional features beyond the scope of the tutorial. Look around the dataset and familiarize yourself with it a little bit. Run a few filter queries such as
{"price": {"$lt":10}}
or {"review_scores.review_scores_rating":100}
to find some really affordable properties or some of the highest rated ones respectively.Now that we have our dataset ready, let's get to writing some Python.
To get started with Starlette, we'll need to install it. Python's pip package manager can help with this. Open up your terminal and run the command
pip3 install starlette
. Starlette by itself is not a server so we'll need to install an ASGI server as well. There's many options like uvicorn, daphne, or hypercorn, and for our tutorial we'll use uvicorn. If you don't already have uvicorn installed, run the command pip3 install uvicorn
and we'll be off to the races.While we're installing dependencies, let's install one final one that we'll need for our application and that is Jinja. Jinja is a templating engine for Python that we'll use to render our application to the browser. Run
pip3 install jinja2
to install it. This is an optional dependency for the Starlette framework, if you're just planning on using Starlette to serve an API with JSON responses for example, then you could omit this step, but for our tutorial, we'll make use of it.Now that we have our dependencies installed, we're ready to write some code. Create a new python file called
app.py
. This will be our main entry point into our Starlette application. To get started, let's setup the most basic Starlette application to ensure that all of our dependencies were installed correctly. Write the following code:1 from starlette.applications import Starlette 2 from starlette.routing import Route 3 from starlette.responses import PlainTextResponse 4 5 async def homepage(request): 6 return PlainTextResponse("Hello from Starlette") 7 8 app = Starlette(debug=True, routes=[ 9 Route('/', homepage), 10 ])
The code above imports the Starlette framework, or various toolkit components if you will, such as routing and responses. Then on line 8 we create an instance of our Starlette application. For our app we'll want the debugging to be on so we have detailed error reports, and for the second parameter we pass in our routes. For this test app, we're only going to have one route and we'll call it
homepage
. Finally, we implement this homepage route and all it does is display the message "Hello from Starlette" to the users browser.Let's make sure our app works. Since we installed uvicorn as our ASGI server, we'll use it to start up our application. In the terminal execute the command
uvicorn app:app
in the directory where your app.py
file resides. This will start up the default server on localhost:8000
. You will get a message in the terminal saying that the Uvicorn server is running if everything went ok, if you have issues at this point, ensure that all of the above pip packages are installed. Check out the uvicorn docs page for troubleshooting.If all went well and the server is up and running, navigate to
localhost:8000
in your browser and you'll see the message "Hello from Starlette." Congrats! You just built your first Starlette application. Now let's build something real.We have a basic Starlette app up and running, now let's expand on it for our tutorial application. While we certainly could add all of our code in this single
app.py
file, that wouldn't be an ideal developer experience. So let's define our requirements and break up our application into multiple files.We know we'll have multiple routes, so let's create a
routes.py
file to house them. Our routes will communicate with our database and rather than creating a connection to our cluster in each route, we'll create a connection pool and pass it down as middleware, so let's also create a middleware.py
file. Next, as we saw in our dataset, we won't work with the entire document, so let's also create a models.py
file that will hold our models. Finally, it looks like we're building an MVC type application where we have models, our routes are our controllers, so let's add a templates
directory, that will hold our views.Now that we have our templates directory created, let's decide on what our application will do. Our app will have three different views:
- A homepage that displays listings
- A listings page that displays additional details on a particular property
- A confirmation page that is displayed when a user books a property
In the
templates
directory, let's create a index.html
, listing.html
, and confirmation.html
file for our three views. In each of the files, let's just create a skeleton of the page we'll want to render.1 <html> 2 <head> 3 <title>MongoBnB | Home</title> 4 <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"> 5 </head> 6 <body> 7 <h1>Home!</h1> 8 </body> 9 </html>
Repeat for both the
listing.html
and confirmation.html
pages. Notice that we're also bringing in TailwindCSS. Tailwind will allow us to better style our pages without writing a bunch of custom CSS.Since we have our skeleton views created, let's open up the
routes.py
file and hook up our views to our controllers. In the routes.py
file, add the following code:1 from starlette.templating import Jinja2Templates 2 3 templates = Jinja2Templates(directory='templates') 4 5 async def homepage(request): 6 return templates.TemplateResponse('index.html', {'request': request}) 7 8 async def listing(request): 9 return templates.TemplateResponse('listing.html', {'request': request}) 10 11 async def confirmation(request): 12 return templates.TemplateResponse('confirmation.html', {'request': request})
In the code above, we are importing Jinja2 templates and setting the default directory where our views will reside. Then, we create three async functions for our three controllers. At the moment we are not doing any logic, we're just serving up static html content. The final piece of the puzzle will be hooking up these new routes to our Starlette application. Open up
app.py
and make the following adjustments to the codebase:1 from starlette.applications import Starlette 2 from starlette.routing import Route 3 from routes import homepage, listing, confirmation 4 5 app = Starlette(debug=True, routes=[ 6 Route('/', homepage), 7 Route('/listing/{id}', listing), 8 Route('/confirmation/{id}', confirmation), 9 ])
We are replacing our default routing with the new routes we created in the
routes.py
file. Restart the uvicorn server and try navigating to localhost:8000
, localhost:8000/listing/1
, and localhost:8000/confirmation/1
and all three of these routes should load and display the message you created earlier.Name a more iconic duo than Python and SQLAlchemy. Fortunately, Starlette is not opinionated about which database technology you should work with and doesn't prefer SQL over NoSQL. For our tutorial, since we have our dataset in MongoDB Atlas, we'll use Mongo and the excellent pymongo package.
If you don't already have pymongo installed, do so now by running
pip3 install pymongo
. Once pymongo is installed, open up the middleware.py
file we created earlier. Here is where we'll instantiate our connection to MongoDB so that we can interact with the dataset and populate our application with real data.1 from starlette.middleware import Middleware 2 from starlette.middleware.base import BaseHTTPMiddleware 3 import pymongo 4 import ssl 5 6 class DatabaseMiddleware(BaseHTTPMiddleware): 7 async def dispatch(self, request, call_next): 8 client = pymongo.MongoClient("{YOUR-CONNECTION-STRING}", ssl_cert_reqs=ssl.CERT_NONE) 9 db = client["sample_airbnb"] 10 request.state.db = db 11 response = await call_next(request) 12 return response 13 14 middleware = [ 15 Middleware(DatabaseMiddleware) 16 ]
The code above will create a new middleware function called
DatabaseMiddleware
. This function will use pymongo to establish a connection to our MongoDB Atlas cluster. Next, we'll want to connect just to our sample_airbnb
database. Before the result of this function is passed down to the individual controller, we'll add a new state variable named db
to our request and send it down to the next middleware or the final controller function. In our case, we'll only have this one middleware function that gets executed.Now that we have our middleware function defined, we'll create a new instance of Middleware, pass in our newly created
DatabaseMiddleware
and store it in a middleware variable. To use this middleware in all of our routes, we'll open up app.py
and make the following adjustments to our Starlette instantiation:1 from starlette.applications import Starlette 2 from starlette.routing import Route 3 from middleware import middleware 4 from routes import homepage, listing, confirmation 5 6 app = Starlette(debug=True, routes=[ 7 Route('/', homepage), 8 Route('/listing/{id}', listing), 9 Route('/confirmation/{id}', confirmation), 10 ], middleware=middleware)
With the middleware in place, we are ready to start implementing our controller functions. This middleware will now execute for all of our functions. We could get granular and pass it only for specific routes, but since all of our routes will be talking to the database, it makes sense to apply this middleware globally. We will be able to access our database in each route via
request.state.db
.We know what our data model looks like in MongoDB, but our Starlette application does not. For this app, we'll only have one model, but we need to define it. Open up
models.py
and add the following code:1 class Property(): 2 def __init__(self, id, name, summary, address, price, cleaning_fee, guests, image, amenities): 3 self.id = id 4 self.name = name 5 self.summary = summary 6 self.address = address 7 self.price = price 8 self.cleaning_fee = cleaning_fee 9 self.guests = guests 10 self.image = image 11 self.amenities = amenities
This code will allow us to create instances of a property that contain only a subset of all the data we get back from our MongoDB database.
We have all the groundwork done, now we can implement our controllers. The first one we'll tackle is the homepage. Open up
routes.py
and add the following code:1 from starlette.templating import Jinja2Templates 2 from models import Property 3 4 async def homepage(request): 5 data = request.state.db.listingsAndReviews.find({'cleaning_fee':{'$exists': True}}, limit=15) 6 7 response = [] 8 9 for doc in data: 10 response.append( 11 Property( 12 doc['_id'], 13 doc['name'], 14 doc['summary'], 15 doc['address']['street'], 16 str(doc['price']), 17 str(doc['cleaning_fee']), 18 str(doc['accommodates']), 19 doc['images']['picture_url'], 20 doc['amenities'] 21 ) 22 ) 23 24 return templates.TemplateResponse('index.html', {'request': request, 'response': response})
Let's break down the code we added for the homepage so far. On the first line we are using the
request.state.db
property passed down from the middleware which contains our MongoDB connection. We are going into the listingsAndReviews
collection and running a find()
operation. We want to return 15 properties and the only filter we're applying is that a cleaning_fee
property must exist on the document.Once this executes we will receive a Cursor that contains our data. Next we'll iterate over that Cursor and create new instances of the Property class for each document in the cursor. Each Property will then be appended in a response list, and this response list will be sent alongside our request to our Jinja template where we'll have access to all the data.
Let's return to our
index.html
file now and update it with the actual UI we want to build.1 <html> 2 <head> 3 <title>MongoBnB | Home</title> 4 <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"> 5 </head> 6 <body> 7 8 <div class="container mx-auto"> 9 <div class="flex"> 10 <div class="row w-full text-center my-4"> 11 <h1 class="text-4xl font-bold mb-5">MongoBnB</h1> 12 </div> 13 </div> 14 <div class="flex flex-row flex-wrap"> 15 {% for property in response %} 16 <div class="flex-auto w-1/4 rounded overflow-hidden shadow-lg m-2"> 17 <img class="w-full" src="{{ property.image }}"> 18 <div class="px-6 py-4"> 19 <div class="font-bold text-xl mb-2">{{ property.name }} (Up to {{ property.guests }} guests)</div> 20 <p>{{ property.address }}</p> 21 <p class="text-gray-700 text-base"> 22 {{ property.summary }} 23 </p> 24 </div> 25 26 <div class="text-center py-2 my-2 font-bold"> 27 <span class="text-green-500">${{ property.price }}</span>/night (+<span class="text-green-500">${{ property.cleaning_fee }} </span>Cleaning Fee) 28 </div> 29 30 <div class="text-center py-2 my-2"> 31 <a href="/listing/{{property.id}}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Details</a> 32 <a href="/confirmation/{{property.id}}" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Book</a> 33 </div> 34 </div> 35 {% endfor %} 36 </div> 37 </div> 38 </body> 39 </html>
Save both the
index.html
file and the routes.py
file. Restart the uvicorn server and navigate to localhost:8000
. The result displayed should look like the image below:Excellent! Our homepage is looking good. It's showing 15 results that are coming from the MongoDB database. Clicking on Details or Booking just takes you to an empty HTML page for now, but that's ok, we're not there yet. Before we implement those, let's also add some filters to our homepage to help our visitors discover additional properties.
In the
index.html
page add the following code underneath the <h1 class="text-4xl font-bold mb-5">MongoBnB</h1>
tag:1 <div class="inline-flex"> 2 <a href="/" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-l"> 3 Featured 4 </a> 5 <a href="/?filter=under-100" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-r"> 6 Under 100 7 </a> 8 <a href="/?filter=highly-rated" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-r"> 9 Highly Rated and Cheap 10 </a> 11 <a href="/?filter=surprise" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-r"> 12 Surprise Me 13 </a> 14 </div>
What this will do is pass a query parameter called
filter
with a pre-defined filter. The filters will be for properties under $100, highly rated properties, and a surprise that you can implement yourself. Let's open up our routes.py
filter and edit the homepage
route to support these filters.1 async def homepage(request): 2 try: 3 filter = request.query_params['filter'] 4 5 if filter == 'under-100': 6 data = request.state.db.listingsAndReviews.find({'$and':[{'cleaning_fee':{'$exists': True}},{'price': {'$lt': 100}}]}, limit=15) 7 elif filter == 'highly-rated': 8 data = request.state.db.listingsAndReviews.find({'$and':[{'cleaning_fee':{'$exists': True}},{'price': {'$lt': 100}},{'review_scores.review_scores_rating': {'$gt': 90}}]}, limit=15) 9 elif filter == 'surprise': 10 data = request.state.db.listingsAndReviews.find({'cleaning_fee':{'$exists': True},'amenities': {'$in': ["Pets allowed", "Patio or balcony", "Self check-in"]}}, limit=15) 11 except KeyError: 12 data = request.state.db.listingsAndReviews.find({'cleaning_fee':{'$exists': True}}, limit=15) 13 14 response = [] 15 16 for doc in data: 17 response.append( 18 Property( 19 doc['_id'], 20 doc['name'], 21 doc['summary'], 22 doc['address']['street'], 23 str(doc['price']), 24 str(doc['cleaning_fee']), 25 str(doc['accommodates']), 26 doc['images']['picture_url'], 27 doc['amenities'] 28 ) 29 ) 30 31 return templates.TemplateResponse('index.html', {'request': request, 'response': response})
Our homepage now supports multiple different filters. If a user visits just the homepage, they'll see the original results, but clicking on of the filter buttons will return a different set of results depending on the type of filter selected. Pymongo supports all of MongoDB's filtering functionality in the familiar syntax. Check out the examples above for various ways to write filters.
Make the changes above, restart your uvicorn server, and navigate to
localhost:8000
to see the results.Our homepage contains a lot of information on multiple properties. The listing page will contain additional information on the selected property. Open up the
routes.py
file and update the listing definition to as follows:1 async def listing(request): 2 id = request.path_params['id'] 3 4 doc = request.state.db.listingsAndReviews.find_one({'_id': id}) 5 6 response = Property( 7 doc['_id'], 8 doc['name'], 9 doc['summary'], 10 doc['address']['street'], 11 str(doc['price']), 12 str(doc['cleaning_fee']), 13 str(doc['accommodates']), 14 doc['images']['picture_url'], 15 doc['amenities'] 16 ) 17 18 return templates.TemplateResponse('listing.html', {'request': request, 'property': response})
Here we are utilizing the
request
object to get the id
of the property we wish to see extra details for. From there, we run a find_one
method that returns just a single document from our MongoDB database. Finally, we take our document and transform it to a new Property class and send it to our template via the property
variable.Open up
listing.html
and update the content to:1 <html> 2 <head> 3 <title>MongoBnB | Property - {{ request.path_params['id'] }}</title> 4 <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"> 5 <style> 6 img { 7 width: 500px !important; 8 margin: 0 auto; 9 } 10 </style> 11 </head> 12 <body> 13 14 <div class="container mx-auto"> 15 <div class="flex"> 16 <div class="row w-full text-center my-4"> 17 <h1 class="text-4xl font-bold">MongoBnB</h1> 18 <a href="/">Back</a> 19 </div> 20 </div> 21 <div class="flex flex-row flex-wrap"> 22 23 <div class="flex-auto w-1/4 rounded overflow-hidden shadow-lg m-2"> 24 <img class="w-full" src="{{ property.image }}"> 25 <div class="px-6 py-4"> 26 <div class="font-bold text-xl mb-2">{{ property.name }} (Up to {{ property.guests }} guests)</div> 27 <p>{{ property.address }}</p> 28 <p class="text-gray-700 text-base"> 29 {{ property.summary }} 30 </p> 31 32 </div> 33 <div class="px-6 py-4"> 34 {% for amenity in property.amenities %} 35 <span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 m-1">{{ amenity }}</span> 36 {% endfor %} 37 </div> 38 39 <div class="text-center py-2 my-2 font-bold"> 40 <span class="text-green-500">${{ property.price }}</span>/night (+<span class="text-green-500">${{ property.cleaning_fee }} </span>Cleaning Fee) 41 </div> 42 43 <div class="text-center py-2 my-2"> 44 <a href="/confirmation/{{property.id}}" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Book</a> 45 </div> 46 </div> 47 </div> 48 </div> 49 </body> 50 </html>
Save both of the files, restart your server, and navigate to
localhost:8000
. Click on the Details button of any property you wish to see expanded information on. The page should look like this:Finally, we'll implement the booking confirmation page. This will be a simple UI, but on the backend, we'll do something we haven't done yet, which is save data to our MongoDB database. Open up the
routes.py
file and add the following implementation for the confirmation route:1 async def confirmation(request): 2 id = request.path_params['id'] 3 4 doc = request.state.db.bookings.insert({"property": id}) 5 6 return templates.TemplateResponse('confirmation.html', {'request': request, 'confirmation': doc})
Like in the previous route, we are making use of route parameters to capture the
id
of the booking. We are then using request.state.db
to access our MongoDB database, but now we are accessing the bookings
collection, which does not exist. That's ok, MongoDB will create a bookings collection for us in the sample_airbnb
database, and since we are using insert, we'll add a document that has one attribute called property
and we'll set it to the id of the listing.Since we are not creating one ourselves, MongoDB will automatically create an
_id
attribute for us, and that's what will be returned and stored in the doc
variable, that we'll pass to our template.Open up
confirmation.html
and add the following UI code:1 <html> 2 <head> 3 <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"> 4 <style> 5 </style> 6 </head> 7 <body> 8 9 <div class="container mx-auto"> 10 <div class="flex"> 11 <div class="row w-full text-center my-4"> 12 <h1 class="text-4xl font-bold">MongoBnB</h1> 13 <a href="/">Back</a> 14 </div> 15 </div> 16 <div class="flex flex-row flex-wrap"> 17 18 <div class="flex-auto w-1/4 rounded overflow-hidden shadow-lg m-2"> 19 <div class="px-6 py-4 text-center"> 20 <h1 class="text-4xl">Confirmed!</h1> 21 <p>Your booking confirmation for {{request.path_params['id']}} is <span class="font-bold">{{confirmation}}</span></span></p> 22 </div> 23 </div> 24 </div> 25 </div> 26 </body> 27 </html>
We are treating our unique
_id
property as the confirmation code for the booking. Restart the server and go ahead and book a property! You should see the following UI:Our Starlette application is complete. We show our users properties they may be interested in, allow them to drill down and view more information, and book the perfect property for their stay. But what if we wanted to go an extra step and add some more functionality?
Here's what I'm thinking. Once a user books their stay, why don't we send them a text message confirming their booking. Knowing that our visitors are likely on the move, why don't we also allow them to cancel their booking by replying to the initial text message in case they change their mind? We're all about giving our users a great experience after all!
To do this we'll make use of the MongoDB Stitch platform. Stitch is a serverless platform that integrates into MongoDB and gives us a plethora of functions, triggers, 3rd party service integrations and more. Since we're talking about texting our users, I think it makes sense to use Stitch's Twilio integration.
To create a new MongoDB Stitch application, we'll go into our MongoDB Atlas dashboard. Select Stitch from the left-hand menu and click the New Application button. Give your application a name, link it to your cluster that has the
sample_airbnb
database and hit Create. MongoDB Atlas will take a few seconds to set up your Stitch application and redirect you to it.Once the MongoDB Stitch dashboard loads up, select 3rd Party Services from the menu and then Twilio. Here you will be asked to name your Twilio service for Stitch as well as provide your Twilio Account SID. You will need an active Twilio account to get the Account SID, and if you don't have one head on over to Twilio to sign up and create your account.
Now that we have our Twilio service created, select the Functions menu item. Click the Create New Function button, name your function and for the settings you can leave all the defaults, but navigate to the Function Editor tab and add the following code:
1 exports = function(arg){ 2 const twilio = context.services.get("Starlette"); //The name of your Twilio service 3 const args = { 4 to: "+19999999999", // The phone number you want the SMS to send to 5 from: "+19999999999", // Your Twilio phone number 6 body: `Greeting from MongoBnB! 7 Your reservation is confirmed. If you need to cancel send a message with your booking id: ${arg.fullDocument._id}` 8 }; 9 twilio.send(args); 10 return {arg: arg}; 11 };
This function will send the user an SMS text message when a new booking is created. For this tutorial, we'll just send the text message to a predetermined phone number, and I'll leave it to you as homework to figure out how to add a user-defined phone number.
Save the function. By default this function will never be called. We have to create a trigger to activate it. Navigate to the Triggers section of the Stitch dashboard. Hit the Add a Trigger button and make the following selections:
- Name - name the function whatever you want
- Cluster Name - this will default to the cluster you selected when creating the Stitch application
- Database Name - be sure to select
sample_airbnb
- Collection Name - be sure to select
bookings
- Operation Type - be sure to select only Insert
- Full Document - set to On
- Function - select the name of the function you created above
Once these settings are in place hit the Save button.
Now our serverless function should be ready to go. Hit the Review & Deploy Changes button in the top blue bar. Once the changes are deployed, go back to your Starlette application on
localhost:8000
.Select a booking that pleases you and click on the Book button. Immediately you will see the confirmation page we built earlier, but in a few seconds, you will also receive a text message telling you that the booking has been created.
Our final piece of functionality in MongoDB Stitch will be to allow the user to send us the confirmation id in the event they want to cancel their booking. To do this, let's navigate back to 3rd Party Services and click our Twilio service we created earlier. From here, we'll click on the Add Incoming Webhook button.
We can leave all the default settings as is but what we'll want to take note of on this page is the Webhook URL. Copy it and save it as we'll need it in just a few minutes. Navigate to the Function Editor tab and add the following code:
1 exports = function(payload) { 2 const mongodb = context.services.get("main"); // The name of your MongoDB cluster 3 const bookings = mongodb.db("sample_airbnb").collection("bookings"); 4 5 bookings.deleteOne({_id: new BSON.ObjectId(payload.Body)}) 6 7 const twilio = context.services.get("Starlette"); // The name of your Twilio service 8 const args = { 9 to: "+19999999999", // The number to send the SMS message to 10 from: "+19999999999", // Your Twilio number 11 body: `Reservation ${payload.Body} has been cancelled. See you next time!` 12 }; 13 twilio.send(args); 14 return payload.Body 15 };
This function will take the body of the SMS message, which for all intents and purposes we'll assume to just be the reservation
_id
, and if it is, we'll go into our bookings
collection and delete that record. Once that is done, we'll respond back to the user telling them that their reservation has been cancelled.This function won't fire automatically though. To get it to work, we'll need to tell Twilio about our function. Log into your Twilio dashboard and navigate to the Phone Numbers service. Select the phone number you wish to use and scroll the the bottom of the page to find the Messaging settings. In the A Message Comes In setting, from the drop down select Webhook and for the value paste in your MongoDB Stitch Webhook URL we saved from earlier. That's it. Now it should all work!
In today's tutorial we learned about Starlette, an ASGI framework for building performant Python applications. We connected our Starlette application to a MongoDB database hosted by MongoDB Atlas. Finally, we enhanced our application using MongoDB Stitch, a serverless platform made for MongoDB, by adding Twilio SMS functionality. I hope you found this tutorial helpful. If you would like to clone the completed code, you can get it from GitHub. Be sure to sign up for MongoDB Atlas as it will give you access to the dataset for the tutorial. Happy coding!