npm i
npm run start
The API is then available at http://localhost:3000
Run unit tests :
npm run test
Run end-to-end tests :
npm run test:e2e
Generate a new API key with the /auth/new-key
endpoint. See details in the Endpoints section below.
To call authenticated endpoints, you must include the 2 following headers to your request:
X-GWR-Key: <your private API key>
X-GWR-Id: <the associated partner id>
Note
This is a helper endpoint. It was not designed as an actual way to generate a new key for a partner in a production scenario. That is why it does not require any authentication, does not validate incoming data, and is not covered by tests. See Security > Improvements for more details on what the production implementation should be.
Method | Route |
---|---|
POST | /auth/new-key |
Parameter | Description |
---|---|
partnerId | A string to identify the partner account associated with the newly generated key. |
{
"partnerId": "test-id"
}
lOQFlz/7k21Aph+dtqWtXKlKap+UfXW4a5m0JoThd1JisFVQWgv0BPFpFmF5GJoG
โ ๏ธ Save this key as there is no way to see it again.
Note
This is the main endpoint of the project receiving client data from GWR partners
Method | Route |
---|---|
POST | /booking |
Parameter | Description |
---|---|
Client email. Must be a valid email address | |
language | Client language. Must be a valid locale |
countryOfOrigin | Client origin. Must be a valid ISO-3166-1 2 letters country code |
countryOfDestination | Client destination. Must be a valid ISO-3166-1 2 letters country code |
travelDateStart | Travel start date. Must be a valid ISO-8601 date |
travelDateEnd | Travel end date. Must be a valid ISO-8601 date |
{
"email": "[email protected]",
"language": "fr",
"countryOfOrigin": "us",
"countryOfDestination": "nz",
"travelDateStart": "2023-12-22",
"travelDateEnd": "2024-01-22"
}
To authenticate, GWR partners must use a secret API key on every request. The advantages of this solution are:
- it is the simplest option to implement on the client side
- several API keys can be associated with the same partner. This is useful to grant different authorization scopes, and to rotate keys without down time.
- the keys can be easily revoked
The main drawback is that it can put a lot of pressure on the database and add latency if the traffic is important as a query is added for each request.
The scenario does not imply particularly high traffic, so this solution seems acceptable. Moreover, the latency can be minimized with the following improvements.
To reduce the load on the database and the latency, we could :
- cache key hashes in memory, as close to the client as possible to reduce the number of calls to the database
- add a checksum at the of the key (eg.
<private-key>_<checksum>
) to reject malformed keys without the need for a database query
Another improvement would be to combine the id and key in a single token. This would make implementation even easier for partners. We need to keep both parts distinguishable though, as the keys are hashed with a salt. Without an id, checking the validity of a key would imply going through the whole table of key hashes one by one which is not acceptable is terms of latency.
Lastly, this implementation only focuses on the requests authentication, not on API keys management. In a production scenario, we would have partner accounts associated to a randomly generated id, expiration dates on keys, etc.
Another implementation I considered was to have a dedicated Auth endpoint to generate short-lived bearer tokens used to authenticate subsequent requests.
sequenceDiagram
Client->>+API: POST /auth<br />X-GWR-Id: <secret-key>
API->>DB: Find secret key
DB->>API: Found
API->>-Client: HTTP 200 OK<br />{ token : afe12...tz }
loop For 5 min
Client->>+API: POST /booking<br />Authorization: Bearer afe12...tz
API->>API: Validate token
API->>-Client: HTTP 201 Created
end
This implementation would greatly reduce the load of the database in the case of a high frequency of request from partners (several requests in the time-to-live of a token).
The main drawbacks of this implementation are that :
- it is a more complicated flow for partners to implement
- it is more difficult to quickly revoke the tokens
I considered we were in a low frequency scenario, hence I favored the first solution.
The data we need to persist is well structured and there is no other specific constraint so a relational database is the best choice in my opinion.
To simplify the setup of the project, I chose SQLite which requires virtually no setup and is very light. For a real production project with no specific constraint, the first choice would have certainly been a system with more features, like PostgreSQL.
The /booking
endpoint does little more than validating the format of the parameters. It would make sense to add more business logic validation.
For example, the language
parameter must be any locale. In real life, there would probably be a few supported languages only.