DEV Community

loading...
Cover image for Manage a Pool of Phone Numbers With Node.js
Vonage

Manage a Pool of Phone Numbers With Node.js

Kevin Lewis
Developer Advocate at Orbit. Developer Events Specialist. #YouGotThisConf Organiser.
Originally published at nexmo.com ・9 min read

You may not always be near your office phone, and when this is the case, customers may struggle to get in touch with you. In this tutorial, we’ll be building an application that uses the Number Management API for Vonage APIs to manage multiple masked phone numbers. Each number will redirect calls to another number, such as a private mobile that can be used from home.

We’ll also make sure that users of our application can only see numbers bought and managed by it, rather than every number in your Vonage API account. Finally, we’ll do some work to make sure that only users you know are given access and that it isn’t accessible from the public web without a password.

The final application screenshot, showing a pool of phone numbers and management options including update and delete

Can I Use This Project Now?

The completed code for this project is in Glitch. You can visit the project, click the Remix to Edit button in the top-right, and add your own credentials to the 🔑.env file. You can then use the project right away by clicking the Show button at the top of the page.

You can also find the completed code on GitHub at https://github.com/nexmo-community/phone-number-pool-manager-node

Prerequisites

Note: Nexmo recently rebranded to Vonage after being acquired in 2016. You'll notice that we make calls to a Nexmo URL in this tutorial - don't be alarmed by this.

Creating a Base Project

There is a boilerplate Glitch project to get you up and running quickly. This application has:

  • Installed and included our dependencies, which you could do in a new Express project by opening the Glitch terminal and typing pnpm install express body-parser cors nedb-promises axios qs express-basic-auth.
  • Created a new nedb database in the .data folder in Glitch. This folder is specific to your version of the application and can't be viewed by others or copied.
  • Initialized a basic Express application, and served the views/index.html file when people navigate to our project URL
  • Included Vue.js and Axios libraries in the index.html file, created a new Vue.js application and added some basic styling in the public/style.css file.

Log in to your Glitch account, and then click on this link to remix (copy) our boilerplate into your account.

Whether you start from scratch or use our boilerplate, you'll need to go to your Vonage API Dashboard, get your API key and secret and put them in your project's 🔑.env file. These values are not publicly visible but can be accessed in your application using process.env.PROPERTY.

Build an Endpoint to Buy Numbers

This endpoint will require a country to be provided, as that is what the Number Management API requires.

Above the final line of your application, include the following code:

app.post('/numbers', async (req, res) => {
    try {
        const { NEXMO_API_KEY, NEXMO_API_SECRET } = process.env;
        const availableNumbers = await axios.get(`https://rest.nexmo.com/number/search?api_key=${NEXMO_API_KEY}&api_secret=${NEXMO_API_SECRET}&country=${req.body.country}&features=SMS,VOICE`);
        const msisdn = availableNumbers.data.numbers[0].msisdn;
        res.send(msisdn);
    } catch (err) {
        res.send(err);
    }
});
Enter fullscreen mode Exit fullscreen mode

When you send a POST request to /numbers, the application will make a GET request to the Number Management API to find an available MSISDN (phone number) and returns the first one.

Open your terminal and run the following command to test the new API endpoint: curl -H "Content-Type: application/json" -X POST -d '{"country": "GB"}' https://YOUR_GLITCH_PROJECT_NAME.glitch.me/numbers, being sure to substitute your Glitch project name. If successful, it should return an available phone number.

Replace res.send(msisdn) with the following:

await axios({
    method: 'POST',
    url: `https://rest.nexmo.com/number/buy?api_key=${NEXMO_API_KEY}&api_secret=${NEXMO_API_SECRET}`,
    data: qs.stringify({ country: req.body.country, msisdn }),
    headers: { 'content-type': 'application/x-www-form-urlencoded' }
});
await db.insert({ msisdn });
res.send('Number successfully bought');
Enter fullscreen mode Exit fullscreen mode

This takes the first MSISDN from the results, purchases it from available account credit, and stores a new database record for the MSISDN. The qs package formats the data as a x-www-form-encoded string, which is what the Number Mangement API requires.

Checkpoint! Repeat the API call to your application from the terminal. You should get a success message, and a new number should be accessible in your Vonage API account.

Note - there are multiple reasons why the Vonage API call might fail in your application that have nothing to do with your code. Check if you can use the Number Management API to get a number in your country. If it still doesn't work, you may require an address and means you must get the number via the Vonage API Dashboard

Build a Frontend to Buy Numbers

Your POST request endpoint might be working fine, but it's time to create a more friendly frontend to use it. Open views/index.html and add the following to your HTML:

<div id="app">
    <h1>Number Manager</h1>
    <section>
        <h2>Buy New Number</h2>
        <input type="text" v-model="country" placeholder="Country Code" />
        <button @click="buyNumber">Buy new number</button>
    </section>
</div>
Enter fullscreen mode Exit fullscreen mode

Update the contents of your <script> to the following:

const app = new Vue({
    el: '#app',
    data: {
        country: ''
    },
    methods: {
        async buyNumber() {
            try {
                if(this.country && confirm('Are you sure you would like to buy a number?')) {
                    await axios.post('/numbers', {
                        country: this.form.country
                    })
                    alert('Successfully bought new number');
                }
            } catch(err) {
                alert('Error buying new number', err);
            }
        }
    }
})
Enter fullscreen mode Exit fullscreen mode

Open the application by clicking Show at the top of your Glitch window. Type "GB" in the box and click "Buy new number." The confirm() function prompts the user with a popup box and is a good practice to avoid accidental purchases. While this application uses Vue.js, you can build any application that can make HTTP requests.

Build an Endpoint to List Numbers

Create a new endpoint in your Express application before the final line of code:

app.get("/numbers", async (req, res) => {
    try {
        res.send('ok');
    } catch (err) {
        res.send(err);
    }
});
Enter fullscreen mode Exit fullscreen mode

At the top of the try block, retrieve all local database entries and all numbers from the Vonage Number Management API for Vonage APIs.

const { NEXMO_API_KEY, NEXMO_API_SECRET } = process.env;
const dbNumbers = await db.find();
const vonageNumbers = await axios.get(`https://rest.nexmo.com/account/numbers?api_key=${NEXMO_API_KEY}&api_secret=${NEXMO_API_SECRET}`);
Enter fullscreen mode Exit fullscreen mode

Then, create a new array which filters vonageNumbers to just those that also appear in the local database. Doing this ensures you only return numbers in this Vonage API account which are managed by this application.

const numbersInBothResponses = vonageNumbers.data.numbers.filter(vonageNumber => {
    return dbNumbers.map(dbNumber => dbNumber.msisdn).includes(vonageNumber.msisdn)
});
Enter fullscreen mode Exit fullscreen mode

Next, create one object which amalgamates both data sources for each number:

const combinedResponses = numbersInBothResponses.map(vonageNumber => {
    return {
        ...vonageNumber,
        ...dbNumbers.find(dbNumber => dbNumber.msisdn == vonageNumber.msisdn)
    }
})
Enter fullscreen mode Exit fullscreen mode

combinedResponses now contains data which is good to send to the user, so replace res.send('ok'); with res.send(combinedResponses);.

Build a Frontend to List Numbers

In your index.html file, create a new method to get the numbers from our Express endpoint:

async getNumbers() {
    const { data } = await axios.get('/numbers')
    this.numbers = data;
}
Enter fullscreen mode Exit fullscreen mode

Update the data object to the following:

data: {
    numbers: [],
    country: ''
}
Enter fullscreen mode Exit fullscreen mode

Load this data by adding a created() function just below your data object:

created() {
    this.getNumbers();
}
Enter fullscreen mode Exit fullscreen mode

Add the following your HTML to display the numbers:

<section>
    <h2>Current Numbers</h2>
    <div class="number" v-for="number in numbers" :key="number.msisdn">
        <h3>{{number.msisdn}}</h3>
        <label for="name">Friendly Name</label>
        <input type="text" v-model="number.name" placeholder="New name">
        <label for="forward">Forwarding Number</label>
        <input type="text" v-model="number.voiceCallbackValue" placeholder="Update forwarding number">
    </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Checkpoint! Click Show at the top of your Glitch editor and open your frontend application. When it loads, you should see your managed phone numbers.

Finally for this section, update the buyNumber() method to include this.getNumbers(); after the success alert(). Once you buy a new number, the list will now be updated without a page refresh.

Building an Endpoint and Frontend to Update Numbers

There are two types of phone number updates this application will support. When updating a number's friendly name, you will be editing entries in the local database, and when updating the forwarding number, you will be updating the number via the Number Management API. Our endpoint must support both and will use the passed data to decide which to update. In server.js add the following:

app.patch("/numbers/:msisdn", async (req, res) => {
    try {
        const { NEXMO_API_KEY, NEXMO_API_SECRET } = process.env;
        if(req.body.name) {
            await db.update({ msisdn: req.params.msisdn }, { $set: { name: req.body.name } })
        }
        if(req.body.forward) {
            await axios({
                method: "POST",
                url: `https://rest.nexmo.com/number/update?api_key=${NEXMO_API_KEY}&api_secret=${NEXMO_API_SECRET}`,
                data: qs.stringify({ 
                    country: req.body.country, 
                    msisdn: req.params.msisdn,
                    voiceCallbackType: 'tel',
                    voiceCallbackValue: req.body.forward
                }),
                headers: { "content-type": "application/x-www-form-urlencoded" }
            })
        }
        res.send('Successfully updated')
    } catch(err) {
        res.send(err)
    }
})
Enter fullscreen mode Exit fullscreen mode

This PATCH endpoint includes the phone number you are updating. If the body contains a name property, the local database will be updated, and if it contains forward, the number settings will be updated via the Number Management API.

In index.html, create the following method:

async updateNumber(number) {
    try {
        const { msisdn, country, name, voiceCallbackValue } = number
        const payload = { country }
        if(name) payload.name = name
        if(voiceCallbackValue) payload.forward = voiceCallbackValue
        await axios.patch(`/numbers/${msisdn}`, payload)
        alert('Successfully updated number');
        this.getNumbers(); 
    } catch(err) {
        alert('Error updating number', err);
    }
}
Enter fullscreen mode Exit fullscreen mode

You must also call this method from the template - which will happen when a user presses enter while focused on one of the text inputs. Update the inputs to the following:

<label for="name">Friendly Name</label>
<input type="text" v-model="number.name" @keyup.enter="updateNumber(number)" placeholder="New name">
<label for="forward">Forwarding Number</label>
<input type="text" v-model="number.voiceCallbackValue" @keyup.enter="updateNumber(number)" placeholder="Update forwarding number">
Enter fullscreen mode Exit fullscreen mode

Checkpoint! Update a friendly name of a number. Then try updating the forwarding number (remember that it must be in a valid format)

Building an Endpoint and Frontend to Cancel Numbers

When a number is no longer required, you may choose to cancel it which immediately releases it from your account. This is the final key part of managing your virtual phone number pool. In server.js add the following above the final line of code:

app.delete("/numbers/:msisdn", async (req, res) => {
    try {
        const { NEXMO_API_KEY, NEXMO_API_SECRET } = process.env;
        await axios({
            method: "POST",
            url: `https://rest.nexmo.com/number/cancel?api_key=${NEXMO_API_KEY}&api_secret=${NEXMO_API_SECRET}`,
            data: qs.stringify({ 
                country: req.body.country, 
                msisdn: req.params.msisdn
            }),
            headers: { "content-type": "application/x-www-form-urlencoded" }
        })
        res.send('Successfully cancelled')
    } catch(err) {
        res.send(err)
    }
})
Enter fullscreen mode Exit fullscreen mode

In index.html add a deleteNumber() method:

async deleteNumber(number) {
    try {
        if(confirm('Are you sure you would like to delete this number?')) {
            const { msisdn, country } = number
            await axios.delete(`/numbers/${msisdn}`, { data: { country } })
            alert('Successfully deleted number')
            this.getNumbers()
        }
    } catch(err) {
        alert('Error deleting number', err);
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, add a button in the template just below the forwarding number input:

<button @click="deleteNumber(number)">Delete number</button>
Enter fullscreen mode Exit fullscreen mode

Checkpoint! Delete a number.

You may have noted that you are not deleting the number from the local database. You may choose to implement this, but as the GET numbers endpoint only returns numbers that exist in both your Vonage API account and the local database, the deleted numbers will not be returned.

Housekeeping

This application is almost complete, but there are a couple of pieces of housekeeping left to do.

Only Allow API Calls From Our Frontend

At the moment, anyone can open their terminal and manage your numbers without permission. Near the top of server.js, just below the app.use() statements, add the following:

app.use(cors({ origin: `https://${process.env.PROJECT_NAME}.glitch.me` }));
Enter fullscreen mode Exit fullscreen mode

process.env.PROJECT_NAME is an environment variable provided by Glitch and is equal to the name of this project. This setting only allows requests from our Glitch URL.

Adding Basic Authentication

Even if people can't access your API from their own applications, they can still stumble across your live site. Fortunately, setting up basic HTTP authentication has just two steps.

Firstly, add a passphrase in your 🔑.env file. Next, add the following line to the bottom of the app.use() statements:

app.use(basicAuth({ users: { admin: process.env.ADMIN_PASSWORD }, challenge: true }));
Enter fullscreen mode Exit fullscreen mode

Now, when you load your application, you will need to give admin as the username and your provided password.

What Next?

This simple application will handle most team's requirements, but there are certainly a few improvements you might make:

  • Only giving certain users the ability to buy numbers
  • Confirming the cost of each number before purchase
  • Adding more data to each number in our local database
  • Better error handling

The completed code for this project is also on GitHub at https://github.com/nexmo-community/phone-number-pool-manager-node.

You can read more about the Number Management API for Vonage APIs through our documentation, and if you need any additional support, feel free to reach out to our team through our Vonage Developer Twitter account or the Vonage Community Slack.

Discussion (0)