Share this blog!

JavaScript Weather App - Part 2 ~ Backend Application

Hello all!

This is part 2 of the article series of building a simple JavaScript weather App.

In part 1, we discussed about the API design perspectives and the architecture of the application. In this article, we will be discussing about the backend implementation of the application, which is essentially about implementing the API we discussed previously.

In this tutorial, we will be:

  1. Initiating a node project
  2. Mapping API endpoints to functions
  3. Configuration and connecting to a database
  4. Querying the database and return result
  5. Testing the API

Things we will NOT be talking about and act as prerequisites:

  • Installing node, MySQL etc.
  • Setting up MySQL or generating mock data
  • Installing packages


A quick recap from the last article, the API we came up with has the following endpoints and use cases:

  1. GET   /locations - Retrieve list of locations that can be forecasted ordered alphabetically by the city
  2. GET   /conditions - Retrieve list of supported weather conditions
  3. GET /forecasts - Retrieve weather forecast of a city defined by query parameter, responding with an array of 10 forecast items. (Optional "limit" query parameter can define the expected forecast count)


Initializing the project

You can simply use npm init to start a node project.

In addition to the usual node project structure, I have added some extra packages so that the services, db management and configurations are separately stored.

Project 
|-------api 
|        |-------swagger.yaml
|-------config 
|        |-------config.js 
|-------db 
|        |-------db.js 
|-------node_modules 
|-------service 
|        |-------condition.service.js 
|        |-------forecast.service.js 
|        |-------location.service.js 
|-------.gitignore 
|-------index.js 
|-------package-lock.json 
|-------package.json

Swagger definition

The content of swagger.yaml can be found in this gist (https://gist.github.com/sachi-d/0f2c0af614723aab0c30095fdfcbe93c) and it was added to the project as a reference to the API definition we came up with. 

You can use swagger to generate the server code from the yaml, but in this tutorial, we will be writing the API functionality by hand simply because our API is not that complicated.

Index.js

The index.js file contains the mappings of the API endpoints and it is essentially the starting point of the application. 

You may notice that the endpoints are mapped into functions in the service layer and finally, the app is listening to the port defined in the configuration. Note how the headers are being used to enable CORS.


const express = require('express');
const app = express();
const mysql = require('mysql');
const config = require('./config/config.js');
const db = require('./db/db.js');
global.db = db;


const {getLocations} = require('./service/location.service');
const {getConditions} = require('./service/condition.service');
const {getForecasts, getForecastsByID} = require('./service/forecast.service');

//enable CORS
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

//map the endpoints with the functions
app.get('/locations', getLocations);
app.get('/conditions', getConditions);
app.get('/forecasts', getForecasts);


const port = config.port;
app.listen(port, () => console.log(`Listening on port ${port}..`));

Configuration file

My config.js file content are as follows, but you can add any kind of configuration/variable options in this, so that you can easily access/update them on the run.

'use strict'

const config = {
 port: process.env.PORT || 8080,
 db: {
  host: 'localhost',
  user: 'mydb-username',
  password: 'mydb-password',
  database: 'mydb-database-name',
  multipleStatements: true
 }
}

module.exports = config

Database connection

I used the db.js file as the database manager, to handle the connection creation.

const mysql = require('mysql');
const config = require('./../config/config.js');

const db = mysql.createConnection (config.db);

// connect to database
db.connect((err) => {
    if (err) {
        throw err;
    }
    console.log('Connected to database');
});

module.exports = db;

Endpoints

I used the service package to collect the data from the database and then expose them through the endpoint. Alternatively, you can achieve more coherency by using a separate DAO layer to retrieve the data and then using a service layer to handle the business logic.

The following code represents the SQL query execution, which retrieves the list of supported weather conditions and locations and then responds to the API requests with the collected data. 


condition.service.js


module.exports = {
 getLocations: (req, res) => {

  //retrieve a list of available locations
  let query = "SELECT * FROM `location` ORDER BY city";

  // execute query
  db.query(query, (err, result) => {
   if (err) {
    console.log(err);
    res.status(400).send("An unexpected error occurred while retrieving locations.");
    return;
   }
   res.send(result);
   return;
  });
 }
};


location.service.js


module.exports = {
 getConditions: (req, res) => {
  //returns a list of supported weather condition names and IDs
  let query = "SELECT * FROM `weathercondition` ORDER BY id ASC";

  db.query(query, (err, result) => {

   if (err) {
    console.log(err);
    res.status(400).send("An unexpected error occurred while retrieving weather conditions");
    return;
   }
   res.send(result);
   return;
  });
 },
};



forecast.service.js

The forecast endpoint implementation seems like the most complicated of all, but when you break down the functionalities, you can easily understand what is happening in the code.

The endpoint requires the location as a query parameter which should be defined by the city, region and the country, separated by commas (inspiration was from Yahoo weather API).

For example,

/weaptherAPI/forecasts?location=Sydney,NSW,Australia 

is a valid API call while

/weaptherAPI/forecasts?location=Sydney,Australia 
/weaptherAPI/forecasts?location=NSW,Australia 
/weaptherAPI/forecasts?location=Sydney

are invalid API calls.

Optionally, the limit query parameter can be used to limit the number of results retrieved, if limit is not defined, 10 results will be returned by default.

When analysing the following code, you may observe how the query parameters are parsed and validated. I have used error messages to handle validation failures. If validations are successful, the parameters will be fed into the SQL query which would return the results as an array.


module.exports = {
 getForecasts: (req, res) => {
  //returns a list of weather forecasts for the specified location

  //if limit is not set, return 10 results by default
  const limit = req.query.limit || 10;
  if(isNaN(limit)){
   res.status(400).send('Invalid request: Invalid limit parameter');
   return;
  }

  const location = req.query.location;

  //location is a required parameter
  if (!location){
   res.status(400).send('Invalid request: Missing location parameter');
   return;
  }


  const parts = location.split(",");

  if(parts.length != 3){
   res.status(400).send('Invalid location parameter');
   return;
  }
  const city = parts[0].trim();
  const region = parts[1].trim();
  const country = parts[2].trim();


  let query = `SELECT w.*, c.conditionName FROM
          (SELECT weather.* FROM weather, location WHERE idLocation = location.id
     AND location.city = "${city}"
     AND location.region = "${region}"
     AND location.country = "${country}"
            AND forecastDate >= UNIX_TIMESTAMP(NOW() - INTERVAL 1 DAY) * 1000
            ORDER BY forecastDate
            LIMIT ${limit}) as w
          LEFT JOIN
            (SELECT id as conditionID, name as conditionName FROM weathercondition) c
            ON w.idCondition = c.conditionID`;


  // execute query
  db.query(query, (err, result) => {
   if (err) {
    console.log(err);
    res.status(400).send("An unexpected error occurred while retrieving weather forecasts");
    return;
   }
   if(result.length == 0){
    res.status(404).send(`No data found for ${location}`);
    return;
   }
            res.send(result);
  });

 }
};



Testing the API

Now your application is ready to run. Use node index.js to start the application in your machine and since all the endpoints are GET type, you can simply use the browser to view the results. 

Access the following URLs to view your results in the browser:

  • http://localhost:8080/locations
  • http://localhost:8080/conditions
  • http://localhost:8080/forecasts?location=any+location+you+have+dummy+data+of

When implementing an API that can be called by a third party application, it is quite important to handle all possible scenarios. These scenarios include both valid and invalid inputs which need to be handled carefully in your implementation. In order to test your application, it is essential to list down the test scenarios that are applicable. The following depicts some of the test scenarios that need to be tested against our application.



Once the test scenarios are identified, testing needs to be carried out. This can be done either manually or programmatically, which we will talk about in a future article.

That's it for the the backend implementation and feel free to share anything you would find useful in the comment section.

Cheers!


Next PostNewer Post Previous PostOlder Post Home

1 comment:


  1. It is very useful and knowledgeable. Therefore, I would like to thank you for the efforts you have made in writing this article.

    WS-C3650-24PS-E
    WS-C3650-24TD-L
    WS-C3650-24PD-S

    ReplyDelete