Thank you for your interest in joining our company as a software engineer. Completing this take-home coding assignment will help us learn about your coding style and determine if you are suitable for the role.
You should expect to spend around 2 hours working on this assignment. Your solutions should demonstrate your ability to write clean, but performant code with concepts and technologies we use frequently:
- Typescript,
- MongoDB, and
- ExpressJS.
You should be able to complete the initial tasks quickly. The final task is more open-ended, giving you a (hopefully) fun opportunity to showcase your creativity as a software developer.
As a new employee, you need to figure out the most efficient way to commute to the office from home. To save money, you decide to commute by bus. To help you find the best bus route from home to the office, you decide to build an application called SGBusGoHome. Your task for this assignment is to complete some REST API routes for the backend of SGBusGoHome. This backend is implemented as a Node ExpressJS application written in Typescript, with data stored in a MongoDB database.
When completing this assignment, please adhere to the following rules:
-
You may refer to online sources, but you may not copy code verbatim. Acknowledge these sources where appropriate in code comments.
-
You may not install any additional NPM packages.
-
You may not download or rely on datasets other than the one provided.
-
You may create additional source code files, but you may not write any additional initialization code. Your code must run only in the request handlers for the routes you must implement as described in the assignment tasks.
A scaffolding for the SGBusGoHome backend has already been implemented
for you. You need only modify the route functions present in
src/routes.ts
.
Before you start coding, install the following dependencies:
Then, install the package dependencies with NPM:
$ npm i
This assignment includes an automated test suite that will compile the application, populate MongoDB with the data described in the next section, invoke your implemented routes, and grade the correctness of the results they return. To run the test suite:
$ npm test
Alternatively, you can start the SGBusGoHome REST API alone for your own testing by running:
$ npm start
The API is accessible at http://127.0.0.1:8080
.
If necessary, you may configure the following runtime parameters of the test suite and standalone REST API server with environment variables:
Parameter | Default Value | Environment Variable to Set |
---|---|---|
MongoDB URL | mongodb://127.0.0.1:27017 |
BUSGOHOME_DB_URL |
MongoDB Database Name | busgohome |
BUSGOHOME_DB_NAME |
REST API Port | 8080 |
PORT |
SGBusGoHome has access to four MongoDB collections, defined in
src/db.ts
. See the dataset documentation for more
information.
Fill in an implementation of the getBusServiceStops
function in
src/routes.ts
to implement the following REST API method
specification.
You must not use the MongoDB aggregation framework to complete this task.
Hint: You should be able to implement this route with only two MongoDB queries.
Return the details of each bus stop along a bus service's route, in order.
GET /services/{ServiceNo}-{Direction}/stops
Parameter Name | Type | Description |
---|---|---|
ServiceNo | string | Service number of the service |
Direction | number | Direction number of the service |
An array of BusStop
objects, in the order of the requested bus
service's route.
If the combination of service number and direction do not correspond to an existing bus service, this method returns a 404 Not Found response with the following JSON response body:
{"error": "Not found"}
GET /services/284-1/stops
[
{
"BusStopCode": "17009",
"RoadName": "Clementi Ave 3",
"Description": "Clementi Int",
"Location": {
"type": "Point",
"coordinates": [
103.76412225438476,
1.31491572870629
]
}
},
{
"BusStopCode": "17211",
"RoadName": "Clementi Ave 4",
"Description": "Bet Blks 315/318",
"Location": {
"type": "Point",
"coordinates": [
103.7658972301256,
1.31828806129678
]
}
},
{
"BusStopCode": "17201",
"RoadName": "Clementi Ave 4",
"Description": "Blk 308",
"Location": {
"type": "Point",
"coordinates": [
103.76734890374861,
1.32055715133061
]
}
},
{
"BusStopCode": "17219",
"RoadName": "Clementi Ave 4",
"Description": "Blk 376",
"Location": {
"type": "Point",
"coordinates": [
103.76615861100824,
1.31820000002277
]
}
},
{
"BusStopCode": "17009",
"RoadName": "Clementi Ave 3",
"Description": "Clementi Int",
"Location": {
"type": "Point",
"coordinates": [
103.76412225438476,
1.31491572870629
]
}
}
]
Fill in an implementation of the getNearbyBusStops
function in src/routes.ts
to implement the following REST API method specification.
Hint: See src/db.ts:69
.
Get a list of bus stops within the requested distance of a position given in longitude, latitutde coordinates.
GET /locations/{Longitude}-{Latitude}/nearbyStops?maxDistance={MaxDistance}
Parameter Name | Type | Description |
---|---|---|
Longitude | number | Longitude of the location |
Latitude | number | Latitude of the location |
MaxDistance | (optional) number | Maximum distance of the search, in kilometers If unspecified, assume a maximum distance of 1 km. |
An array of BusStop
objects. The order of the bus stops is
unimportant.
GET /locations/103.9003-1.309559/nearbyStops?maxDistance=0.3
[
{
"BusStopCode": "82131",
"RoadName": "Dunman Rd",
"Description": "Opp Maranatha Hall",
"Location": {
"type": "Point",
"coordinates": [
103.90034889101835,
1.30955979607535
]
}
},
{
"BusStopCode": "82139",
"RoadName": "Dunman Rd",
"Description": "Maranatha Hall",
"Location": {
"type": "Point",
"coordinates": [
103.89936888898059,
1.30938972202558
]
}
},
{
"BusStopCode": "82149",
"RoadName": "Joo Chiat Rd",
"Description": "Bef Koon Seng Rd",
"Location": {
"type": "Point",
"coordinates": [
103.90136361120997,
1.31101372476887
]
}
},
{
"BusStopCode": "82141",
"RoadName": "Tembeling Rd",
"Description": "Aft Koon Seng Rd",
"Location": {
"type": "Point",
"coordinates": [
103.90248369965865,
1.31084210669638
]
}
}
]
SGBusGoHome allows users to submit a rating from 1 to 5 for each bus service, and to query the average and number of ratings for any bus service.
Fill in an implementation of the getBusServiceRating
function in
src/routes.ts
to implement the following REST API method
specification.
Get the average and number of ratings for a bus service.
GET /services/{ServiceNo}-{Direction}/rating
Parameter Name | Type | Description |
---|---|---|
ServiceNo | string | Service number of the service |
Direction | number | Direction number of the service |
A BusServiceRating
object.
If there are no ratings for the bus service, this method returns a
BusServiceRating
object where AvgRating
and NumRatings
are both
0.
If the combination of service number and direction do not correspond to an existing bus service, this method returns a 404 Not Found response with the following JSON response body:
{"error": "Not found"}
GET /services/135-1/rating
{
"ServiceNo": "135",
"Direction": 1,
"AvgRating": 3.5,
"NumRatings": 505
}
Fill in an implementation of the submitBusServiceRating
function in
src/routes.ts
to implement the following REST API method
specification.
You need to create the BusServiceRating
document for a bus service
if it does not already exist in the collection. If the document does
exist, you must update it atomically.
Hint: Construct your updateOne
query carefully to ensure
atomiticity โ consider using an optimistic update strategy, or
using an aggregation pipeline for the update.
Update the average and number of ratings for a bus service with a new rating submission.
POST /services/{ServiceNo}-{Direction}/rating
Parameter Name | Type | Description |
---|---|---|
ServiceNo | string | Service number of the service |
Direction | number | Direction number of the service |
The Content-Type
header must be specified as application/json
.
interface SubmitRatingRequest {
rating: number;
}
Member Name | Type | Description |
---|---|---|
rating | number | New rating for the bus service Must be a number between 0 and 5. |
Upon success, this method returns a 204 No Content response with no body.
If the combination of service number and direction do not correspond to an existing bus service, this method returns a 404 Not Found response with the following JSON response body:
{"error": "Not found"}
Otherwise, if the request body does not contain a rating
field whose
value is a number between 0 and 5, this method returns a 400 Bad Request
response with the following JSON response body:
{"error": "Invalid rating"}
POST /services/135-1/rating
{
"rating": 4.5
}
Fill in an implementation of the getOppositeBusStops
function in src/routes.ts
to implement the following REST API method specification.
You must implement this route using a single MongoDB aggregation query.
Hint: Codes for opposite bus stop pairs differ only in their final
digit. One bus stop code will end with a 1
, while the opposing stop
in the pair will end with a 9
(e.g., 76051
(Our Tampines Hub) is
located opposite 76059
(Opp Our Tampines Hub) along Tampines Ave 5).
Some bus stop codes end in digits other than 1 and 9. These bus stops have no opposite bus stop, and should be treated as existing in a "pair" comprising only themselves.
Get a list of opposite bus stop pairs along a road.
GET /roads/{RoadName}/stops
Parameter Name | Type | Description |
---|---|---|
RoadName | string | Road name for the query |
An array of BusStop
objects. The bus stops should be sorted in:
-
Ascending order by the bus stop code of the highest-number bus stop in its opposing pair.
Example: The bus stop code of the highest-numbered bus stop in the opposing pair for stops
76051
(Our Tampines Hub) and76059
(Opp Our Tampines Hub) is76059
. -
If two bus stops belong to the same pair, the bus stop with the higher-number code should appear before the bus stop with the lower-number code.
Example:
76059
(Opp Our Tampines Hub) should appear before76051
(Our Tampines Hub).
If there are no bus stops along the given road, this method returns a 404 Not Found response with the following JSON response body:
{"error": "Not found"}
GET /roads/Tampines Ave 5/stops
[
{
"BusStopCode": "75129",
"RoadName": "Tampines Ave 5",
"Description": "Darul Ghufran Mque",
"Location": {
"type": "Point",
"coordinates": [
103.93936521711959,
1.35563642049496
]
}
},
{
"BusStopCode": "75121",
"RoadName": "Tampines Ave 5",
"Description": "Opp Darul Ghufran Mque",
"Location": {
"type": "Point",
"coordinates": [
103.93903255460283,
1.35593392058986
]
}
},
{
"BusStopCode": "75139",
"RoadName": "Tampines Ave 5",
"Description": "Blk 863",
"Location": {
"type": "Point",
"coordinates": [
103.93612503756641,
1.35563964372785
]
}
},
{
"BusStopCode": "75131",
"RoadName": "Tampines Ave 5",
"Description": "Bet Blks 701/702",
"Location": {
"type": "Point",
"coordinates": [
103.93709646027764,
1.35594818346107
]
}
},
{
"BusStopCode": "75149",
"RoadName": "Tampines Ave 5",
"Description": "Blk 867A",
"Location": {
"type": "Point",
"coordinates": [
103.93366388890493,
1.35597194444127
]
}
},
{
"BusStopCode": "75141",
"RoadName": "Tampines Ave 5",
"Description": "Blk 730",
"Location": {
"type": "Point",
"coordinates": [
103.93377888890949,
1.35625000000112
]
}
},
{
"BusStopCode": "75179",
"RoadName": "Tampines Ave 5",
"Description": "Blk 871C",
"Location": {
"type": "Point",
"coordinates": [
103.9317,
1.357
]
}
},
{
"BusStopCode": "75171",
"RoadName": "Tampines Ave 5",
"Description": "UWCSEA",
"Location": {
"type": "Point",
"coordinates": [
103.9313,
1.3575
]
}
},
{
"BusStopCode": "76059",
"RoadName": "Tampines Ave 5",
"Description": "Opp Our Tampines Hub",
"Location": {
"type": "Point",
"coordinates": [
103.94165154852797,
1.35296163572491
]
}
},
{
"BusStopCode": "76051",
"RoadName": "Tampines Ave 5",
"Description": "Our Tampines Hub",
"Location": {
"type": "Point",
"coordinates": [
103.9412639409046,
1.35313809279079
]
}
},
{
"BusStopCode": "76069",
"RoadName": "Tampines Ave 5",
"Description": "Blk 147",
"Location": {
"type": "Point",
"coordinates": [
103.94208629087403,
1.34875342114003
]
}
},
{
"BusStopCode": "76061",
"RoadName": "Tampines Ave 5",
"Description": "Blk 938",
"Location": {
"type": "Point",
"coordinates": [
103.94189805552267,
1.3482019444416
]
}
}
]
Fill in an implementation of the getJourney
function in
src/routes.ts
to implement the following REST API method
specification.
Hint: You may use the already-installed
ngraph.graph
and
ngraph.path
NPM
packages, which provide fast implementations of several path-finding
algorithms.
Get an optimal routing between two bus stops.
This method does not perform an exhaustive search for an optimal routing, but attempts to find the shortest, in terms of estimated time, path between two bus stops using only public buses.
GET /journeys/{OriginStopCode}-{DestinationStopCode}?scenic={ScenicMode}
Parameter Name | Type | Description |
---|---|---|
OriginStopCode | string | Bus stop code of the origin |
DestinationStopCode | string | Bus Stop code of the destination |
ScenicMode | (optional) "true" | "false" | Whether "scenic mode" is enabled (Bonus Task) If scenic mode is enabled, instead of optimizing for estimated time, optimize for the least number of transfers. |
interface JourneySegment {
ServiceNo: string;
Direction: number;
OriginCode: string;
DestinationCode: number;
}
interface Journey {
segments: JourneySegment[];
estimatedTime: number;
}
The response body for this method is a Journey
object.
The JourneySegment
objects in the segments
array satisfy the
following properties:
- The
OriginCode
of the first segment is always the givenOriginStopCode
, - The
DestinationCode
of the last segment is always the givenDestinationStopCode
, and - For any two consecutive segments, the
DestinationCode
of the first segment equals theOriginCode
of the subsequent segment.
The estimated journey time is computed according to the following rules:
- The estimated speed of a public bus is 20 km/h, and
- The estimated time of any transfer between bus services is 10 minutes.
If either the origin code or destination code do not correspond to an existing bus stop, or there exists no journey between them, this method returns a 404 Not Found response with the following JSON response body:
{"error": "Not found"}
GET /journeys/95099-16171
{
"segments": [
{
"ServiceNo": "89",
"Direction": 1,
"OriginCode": "95099", // Opp SAF Ferry Terminal
"DestinationCode": "64009" // Hougang Ctrl Int
},
{
"ServiceNo": "151",
"Direction": 1,
"OriginCode": "64009", // Hougang Ctrl Int
"DestinationCode": "16171" // Yusof Ishak Hse
}
],
"estimatedTime": 140.5
}