> ## Content Index
> Fetch the complete content index at: https://blog.afi.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Let's build a route optimization web service
- URL: https://blog.afi.io/blog/lets-build-a-route-optimization-web-service/
- Published: 2022-01-22T17:50:17.000Z
- Updated: 2026-08-21T05:47:43.000Z
- Description: Coding up a route optimization web service with authentication, persistence and error handling.
- Author: Afian Anwar
- Tags: tech

In my last post, I gave a high-level overview of [how to build a route optimization web service](https://blog.afi.io/blog/route-optimization-web-service-basics/) that uses the Google Maps Directions API as a routing engine. 

Part 1: [Build your own route optimization API with Google Maps](https://blog.afi.io/blog/build-your-own-route-optimization-api-with-google-maps/)  
Part 2: [Creating a route with the Google Maps Directions API](https://blog.afi.io/blog/creating-a-route-with-the-google-maps-directions-api/)  
Part 3: [Optimizing a route with the Google Maps Directions API](https://blog.afi.io/blog/optimizing-a-route-with-the-google-maps-directions-api/)  
Part 4: [Route optimization web service basics](https://blog.afi.io/blog/route-optimization-web-service-basics/)  
**Part 5: Let's build a route optimization web service (this article)**  
Part 6: Putting it all together - testing your route optimization API (coming soon)

In this tutorial, I'll walk through the code that adds authentication, persistence and error handling to the route returned by Google Maps so that you'll get a fully featured route optimization API similar to [Routific](https://dev.routific.com/?ref=blog.afi.io), [GraphHopper](https://www.graphhopper.com/route-optimization/?ref=blog.afi.io) and [OptimoRoute](https://optimoroute.com/api?ref=blog.afi.io) (these are all services hosted on third party servers - if you want your own routing engine running on your infrastructure, Afi Lab's very own [routing engine and route optimization API](https://afi.io/routing%5Fengine?ref=blog.afi.io) might be a better fit).

We'll be using [Nest](https://nestjs.com/?ref=blog.afi.io) (NestJS), a web framework built on Typescript that makes it easy to build efficient, scalable Node.js server-side applications. It forces you to write your code in a standardized way that follows simple database and programming conventions to keep a project structured and readable.

### How our Code is Organized

This isn't a Nest tutorial, but it's worth spending a few minutes explaining how a typical Nest project is structured. As with all my tutorials, you can find [working sample code for this project](https://github.com/afilabs/route-optimization-basics?ref=blog.afi.io) on Github. To get the web service running, [follow the readme instructions](https://github.com/afilabs/route-optimization-basics/blob/master/README.md?ref=blog.afi.io) and set up your [Google Maps](https://developers.google.com/maps/documentation/maps-static/get-api-key?ref=blog.afi.io) keys and database URI (for this project, we are using [MongoDB Atlas](https://www.mongodb.com/atlas/database?ref=blog.afi.io)) in the `.env` file.

**Modules**

Modules are the basic building block of each Nest application and are used to group related features like controllers and services together. They are Typescript files decorated with the @Module() decorator. 

![nestjs code structure and organization into module](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2021/12/Screen-Shot-2021-12-25-at-5.34.47-PM.png)

In this project, we'll be using two modules - `optimization.module.ts` and `users.module.ts` to organize the optimization and user features. Taking a peek at `optimization.module.ts`, here's what we see.

```
import { Module } from '@nestjs/common';
import { OptimizationController } from './optimization.controller';
import { HttpModule } from '@nestjs/axios';

@Module({
  controllers: [OptimizationController],
  imports: [HttpModule],
})
export class OptimizationModule {}

```

The `OptimizationModule` imports `HttpModule` from the [@nestjs/axios library](https://www.npmjs.com/package/@nestjs/axios?ref=blog.afi.io), which allows the `OptimizationModule` to make server side http requests (e.g. to call the Google Maps API).

**Controllers**

In Nest, controllers are responsible for handling incoming requests and returning responses to the client. They are defined using the @Controller() declarator which takes the path for the primary route as its argument. Controllers always belong to a module, which is why we included `OptimizationController` in the `controllers` array in `optimization.module.ts` .

![nodejs controller handling flow](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2021/12/Screen-Shot-2021-12-25-at-6.27.33-PM.png)

Endpoints in the controller are made available to the client by using the decorator e.g. @Get, @Post, @Put and @delete that corresponds to the HTTP request method you want to use. For example, in `optimization.controller.ts` we see the method `@Get('jobs/:id')`:

```
@Get('jobs/:id')
  async getJob(@Param('id') id: string) {
    const job = await this.jobsValidateService.validateJobByJobId(id);

    return {
      id: job.jobId,
      status: 'finished',
      output: JSON.parse(job.output),
    };
  }

```

What this method does is expose the `/jobs/:id` endpoint so that you can make a `GET` request to the `/jobs` endpoint with a given `id` to retrieve a specific optimization job e.g. `/jobs/604a2928-7f6e-4556-994a-4c1114a6d55b`.

**Services**

In Nest, services are used for data storage and retrieval. Instead of sending plain text SQL queries to the database, Nest uses [TypeORM](https://docs.nestjs.com/techniques/database?ref=blog.afi.io) to let us retrieve, save and modify database entries in an object-oriented manner using typescript. Because our data structure is relatively straightforward (we'll mostly use a unique key to retrieve optimization job data), we are going to use a NoSQL database called [MongoDB](https://www.mongodb.com/?ref=blog.afi.io). To keep things simple, we'll host our MongoDB instance in the cloud on a the [Atlas](https://www.mongodb.com/atlas/database?ref=blog.afi.io) database as a service (detailed instructions on how to set this up is included in the [readme file](https://github.com/afilabs/route-optimization-basics/blob/master/README.md?ref=blog.afi.io)).

In the `@Get('jobs/:id')` method above, we call the `validateJobByJobId(id)` method of the `jobsValidateService` to retrieve data from the specified optimization job. We are able to use this service in `optimization.controller.ts` because we imported it with the line `import { JobsValidateService } from '../database/services/jobs.validate.service';` at the very top. Taking a peek at `jobs.validate.service.ts`, we see:

```
async validateJobByJobId(jobId: string) {
    const job = await this.jobModel.findOne({ jobId: jobId }).exec();
    if (!job) {
      throw new BadRequestException([`job not exist`]);
    }
    return job;
  }
```

The `validateJobByJobId` method simply calls `FindOne()` on `jobModel`. This method expects a `jobId` of type `string` and returns a single `job` object (defined in `job.schema.ts`). If the `job` with the specified `jobId` does not exist, we throw a bad request exception with a basic 'job does not exist' error message.

### Creating Users

The users module lets you create users with an email address and password. When a user is created, a static API key is generated that you can use to authenticate with the web service. Let's look at the code to see how all this works - it's a good example of how a typical Nest application is put together. Near the top of `users.controller.ts` we find the `createUser()` method.

### users.controller.ts

```
@Post()
async createUser(@Body() createUserDto: CreateUserDto) {
    return await this.usersService.createNewUser(createUserDto);
}
```

The `@Post()` decorator without any arguments next to `createUser()` means that you can make a `POST` request to `/users` to call the method and create a user. The `@Body` decorator extracts data from the json body of our request in the form of a [data transfer object](https://stackoverflow.com/questions/59397687/what-is-the-purpose-of-a-data-transfer-object-in-nestjs?ref=blog.afi.io) (DTO) which has a schema consistent with `CreateUserDto` ( `create-user.dto.ts` ).

### create-user.dto.ts

[Data transfer objects](https://stackoverflow.com/questions/59397687/what-is-the-purpose-of-a-data-transfer-object-in-nestjs?ref=blog.afi.io) are templates that let developers (or anyone consuming an API) know how the request body should be structured. Here's what `create-user.dto.ts` looks like.

```
export class CreateUserDto {
  @ApiProperty({
    default: 'Afian Anwar',
  })
  @IsNotEmpty()
  readonly name: string;

  @ApiProperty({
    default: 'afian@afi.io',
  })
  @IsNotEmpty()
  @IsEmail()
  readonly email: string;

  @ApiProperty({
    default: '123123123',
  })
  @IsNotEmpty()
  readonly password: string;
}
```

Users are created with a `name` (string), `email` (string), and `password` (string). The DTO is passed into the `createNewUser()` function of `UsersService` which subsequently calls the `registerNewUser()` method in `UsersUtilService`. Let's take a look at what happens there.

```
async registerNewUser(
    registerUserDto: RegisterUserDto,
    fields = {},
): Promise < User > {
    const user = await this.findUserByEmail(registerUserDto.email);
    if (user) {
        throw new BadRequestException([`email already exist`]);
    }

    const token = (await nanoid()) + CryptoService.randomToken(6);
    const tokenEncrypted = CryptoService.encrypt(token);

    const encryptPassword = await bcrypt.hash(registerUserDto.password, 10);
    const values = {
        ...registerUserDto,
        ...fields,
        token: tokenEncrypted,
        password: encryptPassword,
    };

    const createdUser = new this.userModel(values);
    await createdUser.save();

    return createdUser;
}
```

**Authentication** 

In addition to the name, email and password associated with the user, we generate a token that can be used as an API key to authenticate requests to our web service. The two lines of code

```
const token = (await nanoid()) + CryptoService.randomToken(6);
const tokenEncrypted = CryptoService.encrypt(token);
```

generate a 34 character long random string e.g. V2S7y2t5aZ6icuTxlP8a3f2123f07c4af. We then save this information (together with the name, email and password of our user) to the database with

```
const createdUser = new this.userModel(values);
await createdUser.save();
```

Next, let's see how this API key is used to authenticate route requests. 

### Making a Route Request

To make a route request, the user makes a `POST` call to `/tsp-long` and includes his API key in the header ( e.g. `Authorization: V2S7y2t5aZ6icuTxlP8a3f2123f07c4af` ) and the request input in the body.

```
{
	"visits": {
		"A": {
			"location": {
				"name": "Stop A",
				"address": "1200 Third Ave, San Diego, CA 92101, United States"
			}
		},
		"B": {
			"location": {
				"name": "Stop B",
				"address": "707 Tenth Ave, San Diego, CA 92101, United States"
			}
		}
	},
	"fleet": {
		"Afian": {
			"start_location": {
				"id": "afian-start",
				"name": "afian-start",
				"address": "1337 India St, San Diego, CA 92101, United States"
			},
			"shift_start": "08:00"
		}
	}
}
```

We've designed the input as a json object with two fields - `visits` (a mapping of stop names to locations) and `fleet` (a mapping of drivers to their start locations and starting times). The data sent to `/tsp-long` is immediately routed to the `googleDirection()` method of `optimization.controller.ts`.

### optimization.controller.ts

This is where the magic happens. Let's break down what happens to our request input at each stage.

```
@Post('tsp-long')
async googleDirection(@Body() body) {
    const jobId = uuidv4();
    await this.jobsUtilService.createNewJob({
        jobId
    });

    let capacity = Object.keys(body.visits).length;
    if (capacity > 25) {
        return {
            message: 'Too many visits in this request. Please limit your optimization request to 25 visits or less (https://developers.google.com/maps/documentation/directions/usage-and-billing).',
        };
    }
    capacity = Object.keys(body.fleet).length;
    if (capacity !== 1) {
        return {
            message: 'Too many visits in this request. Please limit your optimization request to 25 visits or less (https://developers.google.com/maps/documentation/directions/usage-and-billing).',
        };
    }
    ... 
}
```

The body of our request is captured by the `body` object using Nest's @Body() decorator. [Google Maps limits how many stops and vehicles can be optimized in a single API call](https://developers.google.com/maps/documentation/directions/usage-and-billing?ref=blog.afi.io), so we first need to check that our `visits` objects contains at most 25 stops and that there is only a single driver in the `fleet` object. 

```javascript
https://maps.googleapis.com/maps/api/directions/json?
origin={origin}
&destination={destination}
&waypoints=optimize:true|{first_waypoint}|{second_waypoint}|...|{last_waypoint}
&key=GOOGLE_API_KEY
```

Keep in mind that our goal is to eventually optimize our route using Google Map's Directions API. To do that, we need to format our API call to be [consistent with the Directions API schema](https://blog.afi.io/blog/optimizing-a-route-with-the-google-maps-directions-api/) (above).

```
const points = [];
const visits = body.visits;
const fleets = body.fleet;
const options = body.options;
let driver = null;
const stops = [];

for (const key in visits) {
    const visit = visits[key];
    const address = visit.location?.address;

    if (!address) {
        return {
            message: `Your visit (${key}) is missing the address field.`,
        };
    }

    points.push(address);
    stops.push({
        ...visit,
        key
    });
}
```

First, we are going to iterate over `visits` and save each location's address in the `stops` array. We also check to see that the address field for each field is populated, and if not, return a helpful error message pointing out that a specific visit is missing the address field.

```
const points = [];
const visits = body.visits;
const fleets = body.fleet;
const options = body.options;
let driver = null;
const stops = [];

for (const key in visits) {
    const visit = visits[key];
    const address = visit.location?.address;

    if (!address) {
        return {
            message: `Your visit (${key}) is missing the address field.`,
        };
    }

    points.push(address);
    stops.push({
        ...visit,
        key
    });
}
```

Next, we do the same for `fleets` (note: there should only be a single vehicle in the `fleets` object because the Directions API limits us to optimizing a single origin / destination pair).

```
for (const key in fleets) {
    const fleet = fleets[key];
    const address = fleet.start_location?.address;

    if (!address) {
        return {
            message: `Your fleet (${key}) is missing the address field.`,
        };
    }

    driver = {
        ...fleet,
        key
    };
}
```

Recall, here's what the call to the Directions API looks like:

```javascript
https://maps.googleapis.com/maps/api/directions/json?
origin={origin}
&destination={destination}
&waypoints=optimize:true|{waypoints}
&key={key}
```

Let's work on substituting `{origin}`, `{destination}` and `{waypoints}` with the correct values. 

`{origin}` is simply `driver.start_location.address`,

```
url = url.replace('{origin}', driver.start_location?.address);
```

`{destination}` (if one exists) is `driver.end_location.address`,

```
url = url.replace('{destination}', driver.end_location?.address);
```

and `{waypoints}` is the concatenated list of `stops` joined together by a "|".

```
url = url.replace('{waypoints}', points.join('|'));
```

Once the URL is set up, we [encode it](https://www.freecodecamp.org/news/javascript-url-encode-example-how-to-use-encodeuricomponent-and-encodeuri/?ref=blog.afi.io#:~:text=Why%20do%20we%20need%20to,when%20passing%20into%20a%20URL.) and make a `GET` request, saving the response in a `data` object. We also prepare the `output` with empty objects that will be populated later.

```
url = encodeURI(url);

const response = await this.httpService.get(url).toPromise();
const {
    data
} = response;

if (data.status !== 'OK') {
    return data;
}

const output = {
    status: 'completed',
    fitness: 0,
    unserved: [],
    solution: {},
    polylines: {},
};
```

If you called the Directions API directly, you'll annoyingly find that it does not list the arrival times at each stop. Instead, each segment of the trip is broken down into `legs` with a `duration` object. Inside it is the `value` field which contains the travel time for each `leg` (see example below).

![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2022/01/Screen-Shot-2022-01-21-at-11.07.55-PM.png)

To get the arrival times at each stop, we start with `driver.shift_start` and incrementally add the duration `value` to each stop (we use the `hhmmToMinutes()` and `minutesToHHmm()` helper functions to convert minutes to hh:mm and back) and update the arrival time. We also assume a service time at each stop of 1 minute e.g. if you arrive at the first stop at 08:05, you leave at 08:06.

```
arrivalTime = arrivalTime + Math.round(duration.value / 60);
finishTime = arrivalTime + 1;
```

We then add this data to the `steps` array in a nicely formatted javascript object with `location_id`, `location_name`, `arrival_time` and `finish_time`.

```
const route = data.routes[0];
    const { legs, overview_polyline, waypoint_order } = route;

    const hhmmToMinutes = (hhmm) => {
      const a = hhmm.split(':');
      return +a[0] * 60 + +a[1];
    };
    const minutesToHHmm = (m) => {
      const hours = Math.floor(m / 60);
      const minutes = m - hours * 60;
      return (
        hours.toString().padStart(2, '0') +
        ':' +
        minutes.toString().padStart(2, '0')
      );
    };

    let arrivalTime = hhmmToMinutes(
      driver.shift_start ? driver.shift_start : '08:00',
    );
    let finishTime = arrivalTime + 1;

    const steps = [];
    steps.push({
      location_id: driver.id ? driver.id : driver.key,
      location_name: driver.start_location.name,
      arrival_time: minutesToHHmm(arrivalTime),
      finish_time: minutesToHHmm(finishTime),
    });

    legs.forEach((leg, key) => {
      const { duration } = leg;
      const index =
        typeof waypoint_order[key] === 'undefined'
          ? stops.length - 1
          : waypoint_order[key];
      const visit = stops[index];
      arrivalTime = arrivalTime + Math.round(duration.value / 60);
      finishTime = arrivalTime + 1;

      steps.push({
        location_id: visit.key,
        location_name: visit.location.name,
        arrival_time: minutesToHHmm(arrivalTime),
        finish_time: minutesToHHmm(finishTime),
      });
    });
```

After that, we maps the steps array to the driver's id in `output.solution` and the encoded polylines string (used to draw the route on a map) in `output.polylines`. We then use Javascript's build in `JSON.stringify()` method to convert both to json and save it to the database via `jobsUtilService.updateJobByJobId()`.

```
output.solution[driver.key] = steps;
output.polylines[driver.key] = overview_polyline.points;

await this.jobsUtilService.updateJobByJobId(jobId, {
    input: JSON.stringify(body),
    output: JSON.stringify(output),
});

return {
    job_id: jobId
};
```

Lastly, we return `jobId` to the user, so they get back something like 

```
{	"job_id": "604a2928-7f6e-4556-994a-4c1114a6d55b"	}
```

in the response. You can then make a `GET` request to the `/jobs` endpoint with a given `id` to retrieve a specific optimization job e.g. `/jobs/604a2928-7f6e-4556-994a-4c1114a6d55b`.

Congratulation! You've just built a route optimization web service from the ground up. In our last post of this series, Part 6: Testing our Route Optimization Web Service (coming soon!), we'll do some live testing so you can use the API we've just built in production.

**This article was written by** [**Afi Labs**](https://afi.io/?ref=blog.afi.io)**, a** [**Google Maps Premier Partner**](https://www.afi.io/?ref=blog.afi.io) **and reseller. We build route optimization, navigation, and fleet tracking software on Google Maps, and offer volume pricing on GMP licensing.** [**Talk to an engineer**](https://www.afi.io/contact%5Fus?ref=blog.afi.io) **or** [**follow Afian on LinkedIn**](https://www.linkedin.com/in/afian-anwar/?ref=blog.afi.io)**.**