0

I've created a REST API in Node.js with the PostgresSQL using its pg library and express for REST API creation and routing and body-parser for json. The REST API functions properly, I've tested it on the thunder client of vscode. Here's the middle ware code:

const express = require('express'); 
const bodyParser = require('body-parser'); 
const foodRecipeRoute = require('../router/foodRecipe.router'); 
const path = require('path');

let app = express();

app.use(bodyParser.json()); 
app.use(bodyParser.urlencoded({ extended: false }));

app.use("/foodRecipe", foodRecipeRoute);

const port = 8080 || 8000;

// Connecting port 
app.listen(port, () => { console.log('Port connected to: ' + port) });

// Find 404 and hand over to error handler 
app.use((req, res, next) => { next(console.log("Error occurred")); });

// Index Route 
app.get('/', (req, res) => { res.send('invalid endpoint'); });

// error handler 
    app.use(function(err, req, res, next) {                                                                            console.error(err.message); 
    if (!err.statusCode) err.statusCode = 500;
    res.status(err.statusCode).send(err.message); 
});

// Static build location 
app.use(express.static(path.join(__dirname, 'dist')));

The code of the tasks to be performed in foodDb.tasks.js is as under:

let PostgreSQL = require('pg');
const Pool = PostgreSQL.Pool;

let pool = new Pool({
    user: 'postgres',
    host: 'localhost',
    database: 'postgres',
    password: '6equj5*243',
    port: 5432,
});

/**
 * @param {Request} req
 * @param {Response} res
 */
const hasAppUser = (req, res) => {
    const { appusername, userpassword } = req.body;
    pool.query("select exists(select from appuser where appusername = $1 and userpassword = $2);",
        [appusername, userpassword],
        (error, result) => {
            if (error) throw error;
            res.status(200).json(result.rows[0]);
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const signInUser = (req, res) => {
    const { appusername, userpassword } = req.body;
    pool.query(
        "select username, appusername" +
        " from appuser" +
        " where appusername = $1" +
        " and userpassword = $2;",
        [appusername, userpassword],
        (error, result) => {
            if (error) {
                throw error;
            }
            if (result.rows.length == 0) {
                res.status(404).json({signedIn: false});
            }
            res.status(200).json(result.rows[0]);
        }
    );
};

/**
 * @param {Request} req
 * @param {Response} res
 */
const signUpUser = (req, res) => {
    const { username, appusername, userpassword } = req.body;
    pool.query(
        "insert into appuser (" +
        "  userId," +
        "  username," +
        "  userpassword," +
        "  appusername" +
        ") values (" +
        "   uuid_generate_v4()," +
        "   $1," +
        "   $2," +
        "   $3" +
        ");",
        [username, userpassword, appusername],
        (error, _) => {
            if (error) {
                res.status(400).json({ err: error });
            }
            res.status(201).json({ message: `Created ${appusername} successfully` });
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const createRecipe = (req, res) => {
    const { recipename, ingredientnames, ingredientamounts, appusername } = req.body;

    for (let i = 0; i < ingredientnames.length; i += 1){
        pool.query(
            "insert into recipedetails(detailID, recipeid, ingredientid, ingredientamount, appusername)" +
            " values(uuid_generate_v4(), fetch_recipe_id($1), get_ingredient_id($2), $3, $4);",
            [recipename, ingredientnames[i], ingredientamounts[i], appusername],
            (error, _) => {
                if (error) throw error;
                res.status(201).send(
                    `Created ${recipename} with ${ingredientnames[ingredientnames.length - 1]}' +
                    'as last ingredient by ${appusername}`
                );
            }
        );
    }
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const addIngredientToRecipe = (req, res) => {
    const { recipename, ingredientname, ingredientamount, appusername } = req.body;
    pool.query(
        "Insert into recipedetails(detailId, recipeid, ingredientid, ingredientamount, appusername)" +
        "values(uuid_generate_v4(), get_user_recipe_id($1, $4), get_ingredient_id($2), $3, $4);",
        [recipename, ingredientname, ingredientamount, appusername],
        (err, res) => {
            if (err) throw err;
            res.status(201).send("Insertion successful");
        }
    );
};


/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const getUserRecipies = (req, res) => {
    const { appusername } = req.body;
    pool.query(
        "select recipename" +
        " from recipe" +
        " inner join (" +
        "   select recipeid" +
        "   from recipedetails" +
        "   where appusername = $1" +
        ") as userRecipies" +
        " on recipe.recipeid = userRecipies.recipeid" +
        " group by recipename",
        [appusername],
        (err, result) => {
            if (err) throw err;

            let userAllRecipies = [];
            for (let i = 0; i < result.rows.length; i += 1) {
                userAllRecipies.push(result.rows[i].recipename);
            }
            res.status(200).json({recipies: userAllRecipies});
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const getUserRecipeDetails = (req, res) => {
    const { appusername, recipename } = req.body;
    pool.query(
        "select ingredientname, ingredientamount" +
        " from ingredient" +
        " inner join (" +
        "   select ingredientid, ingredientamount" +
        "   from recipedetailedview" +
        "   where recipedetailedview.appusername = $1" +
        "   and recipedetailedview.recipename = $2" +
        "   group by ingredientid, ingredientamount" +
        ") as userRecipeDetails " +
        "on ingredient.ingredientid = userRecipeDetails.ingredientid;",
        [appusername, recipename],
        (err, result) => {
            if (err) throw err;
            res.status(200).json({ recipeDetails: result.rows });
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const updateUserRecipeIngredientName = (req, res) => {
    const {
        appusername,
        oldingredientname,
        newingredientname,
        recipename
    } = req.body;

    pool.query(
        "update recipedetails" +
        " set ingredientid = get_ingredient_id($3)" +
        " where recipeid = get_user_recipe_id($1, $4)" +
        " and ingredientid = $2;",
        [appusername, oldingredientname, newingredientname, recipename],
        (err, _) => {
            if (err) throw err;
            res.status(201).send(`Updated with ${newingredientname}`);
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const updateUserRecipeIngredientAmount = (req, res) => {
    const {
        appusername,
        recipename,
        ingredientname,
        ingredientamount
    } = req.body;

    pool.query(
        "update recipedetails set ingredientamount = $4" +
        " where recipeid = get_user_recipe_id($1, $2)" +
        " and ingredientid = get_ingredient_id($3);",
        [appusername, recipename, ingredientname, ingredientamount],
        (err, _) => {
            if (err) throw err;
            res.status(201).send(`Updated ingredient amount with: ${ingredientamount}`);
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const updateUserRecipeName = (req, res) => {
    const { appusername, recipename, oldrecipename, newrecipename } = req.body;
    pool.query(
        "update recipedetails set recipeid = get_recipe_id($3)" +
        " where recipeid = get_user_recipe_id($1, $2) and appusername = $1;",
        [appusername, recipename, oldrecipename, newrecipename],
        (err, _) => {
            if (err) throw err;
            res.status(201).send("Updated user recipe name");
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const deleteUserRecipeIngredient = (req, res) => {
    const { appusername, recipename, ingredientname } = req.body;
    pool.query(
        "delete from recipedetails where recipeid = get_user_recipe_id($1, $2)" +
        " and ingredientid = get_ingredient_id($3);",
        [appusername, recipename, ingredientname],
        (err, _) => {
            if (err) throw err;
            res.status(201).send(`deleted ${recipename}'s ingredient ${ingredientname}`);
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const deleteUserRecipeName = (req, res) => {
    const { appusername, recipename } = req.body;
    pool.query(
        "delete from recipedetails where recipeid = fetch_recipe_id($2) and appusername = $1;",
        [appusername, recipename],
        (err, _) => {
            if (err) throw err;
            res.status(201).send(`deleted ${recipename} of appuser ${appusername}`);
        }
    );
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const deleteUserAllRecipies = (req, res) => {
    const { appusername } = req.body;
    pool.query("Delete from recipedetails where appusername = $1;",
    [appusername],
    (err, _) => {
        if (err) throw err;
        res.status(201).send(`Deleted ${appusername} recipies`);
    });
};

/**
 *
 * @param {Request} req
 * @param {Response} res
 */
const deleteUser = (req, res) => {
    const { appusername } = req.body;
    pool.query("Delete from appuser where appusername = $1;",
    [appusername],
    (err, _) => {
        if (err) throw err;
        res.status(201).send(`Deleted ${appusername}`);
    });
};

module.exports = {
    hasAppUser,
    signInUser,
    signUpUser,
    createRecipe,
    addIngredientToRecipe,
    getUserRecipies,
    getUserRecipeDetails,
    updateUserRecipeIngredientName,
    updateUserRecipeIngredientAmount,
    updateUserRecipeName,
    deleteUserRecipeIngredient,
    deleteUserRecipeName,
    deleteUserAllRecipies,
    deleteUser
};

The routes code is as under as well, I've used POST method in the route for most of the fetching operations that's because in flutter's http GET method you've to pass the params in the url/uri of the API, which is quite unsafe, so that's why I went for the POST method so that I may securely make fetching on the flutter side as well which will definitely expose info to malicious intent holders online, when deployed.

The routes code involves the usage of the API tasks code, which I've named as foodDB.tasks.js file. I've already pasted this file's code above. The routes are defined as:

let express = require('express');
let tasks = require('../tasks/foodDb.tasks');

let foodRecipeRoute = express.Router();

// The endpoint to be consumed by client to 
// determine whether the API url is working or not
foodRecipeRoute.get("/", (req, res) => {
    res.status(200).json({hasData: "has data"});
});

foodRecipeRoute.post("/checkUser", tasks.hasAppUser);
foodRecipeRoute.post("/signIn", tasks.signInUser);
foodRecipeRoute.post("/signUp", tasks.signUpUser);

foodRecipeRoute.post("/getRecipies", tasks.getUserRecipies);
foodRecipeRoute.post("/getRecipeDetails", tasks.getUserRecipeDetails);
foodRecipeRoute.post("/createRecipe", tasks.createRecipe);
foodRecipeRoute.post("/addIngredient", tasks.addIngredientToRecipe);

foodRecipeRoute.put("/ingredientNewname", tasks.updateUserRecipeIngredientName);
foodRecipeRoute.put("/ingredientNewAmount", tasks.updateUserRecipeIngredientAmount);
foodRecipeRoute.put("/recipeNewname", tasks.updateUserRecipeName);

foodRecipeRoute.delete("/deleteIngredient", tasks.deleteUserRecipeIngredient);
foodRecipeRoute.delete("/deleteRecipe", tasks.deleteUserRecipeName);
foodRecipeRoute.delete("/deleteRecipies", tasks.deleteUserAllRecipies);
foodRecipeRoute.delete("/deleteUser", tasks.deleteUser);

module.exports = foodRecipeRoute;

For the client (flutter) side of my project, I m first determining whether the API url is fully functional, if so then it can navigate the screen of the concern. The code is in main.dart file, which is as under:

// ignore_for_file: avoid_print

import 'dart:convert';

import 'package:flutter/material.dart';
import "package:http/http.dart" as http;
import 'package:provider/provider.dart';

import 'core/config/api_url.dart';
import 'view/viewmodels/recipies_screen_viewmodel.dart';
import 'view/screens/user_recipies_screens.dart';
import 'view/viewmodels/recipe_ingredient_screen_viewmodel.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ListenableProvider<RecipeIngredientScreenViewmodel>(
          create: (context) => RecipeIngredientScreenViewmodel(),
        ),
        ListenableProvider<RecipiesScreenViewmodel>(
          create: (context) => RecipiesScreenViewmodel(),
        ),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  Future getUrlData() async {
    var response = await http.get(Uri.parse(apiUrl));
    debugPrint(response.statusCode.toString());
    return json.decode(response.body);
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: FutureBuilder(
        future: getUrlData(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return const UserRecipiesScreen();
          }
          return Center(
            child: ListView(
              children: <Widget>[
                ElevatedButton(
                  onPressed: () async {
                    var res = await getUrlData();
                    debugPrint(res.toString());
                  },
                  child: const Text("Press me"),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

The method getUrlData gets the data from the "/" endpoint of the router, foodRecipeRouter in my case. The code is mentioned the routing of endpoints code above, while that of this endpoint is as under:

foodRecipeRoute.get("/", (req, res) => {
    res.status(200).json({hasData: "has data"});
});

It's result is as under using thunder client of vscode:

"/" endpoint result

The result of building this app on web was this:

App result UI

When I click the button, I get:

Flutter Web Bootstrap: Programmatic
The Flutter DevTools debugger and profiler on Chrome is available at:
http://127.0.0.1:9101?uri=http://127.0.0.1:51942/MhxrvaKN7aU=
Error: XMLHttpRequest error.
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 299:10  createErrorWithStack
dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 341:28            _throw
dart-sdk/lib/core/errors.dart 116:5                                           throwWithStackTrace
dart-sdk/lib/async/zone.dart 1378:11                                          callback
dart-sdk/lib/async/schedule_microtask.dart 40:11                              _microtaskLoop
dart-sdk/lib/async/schedule_microtask.dart 49:5                               _startMicrotaskLoop

This has confused me due to the fact that I m making a local REST API, so there's no question of the need of the headers, correct me if I m wrong and help me please regarding this issue. And also please guide me that how to connect a localhost api to a flutter app?

I tried to change my apiUrl from http://localhost:8080/foodRecipe/ to http://192.168.100.4:8080/foodRecipe/ for thinking that localhost is not understood by flutter but all in vain. I make sure that there're no headers checked as required from thunder client.

I refer to this answer but doesn't work, no data was fetched by GET operation on the flutter side.

JackTheCoder
  • 3
  • 1
  • 3
  • in your flutter code i don't see how you pass the "apiUrl" variable you just set it like this Uri.parse(apiUrl) ,can you show us where you declare it ! and also in my opinion it's better to use statefulwidget when you have data that changes in your build context – Ali Ourag Jan 12 '23 at 10:17
  • Please trim your code to make it easier to find your problem. Follow these guidelines to create a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – hungtran273 Jan 12 '23 at 12:55
  • @AliOurag here it is: const apiUrl = "http://192.168.100.4:8080/foodRecipe/"; – JackTheCoder Jan 13 '23 at 11:05
  • to be honest your flutter code is not clear and also not correct ! for example you nested the FutureBuilder inside the home property directly and if your snapshot has Data you are returning the UserRecipiesScreen wish you did it shear the code for it ! and as i see you don't pass any data to it to be displayed !! I suggest that you reformat your code and use statefulwidget wish is better also you can refer to this link : | https://codewithflutter.com/flutter-fetch-data-from-api-rest-api-example/ – Ali Ourag Jan 13 '23 at 14:20
  • @AliOurag the userecipescreen has nothing to do with this end point which I want the response from, and it has nothing to do with the future builder being in the stateless widget, cause I m not rendering any data on the screen, all I want is the confirmation of the response's data existence; cause if it has data then it simple means that the API is up, else it's gonna show the elevated button on the screen – JackTheCoder Jan 13 '23 at 15:52

1 Answers1

0

I just modified the getUrlData, and add the following code:

Future getUrlData() async {
    var request = http.Request(
      'GET',
      Uri.parse(apiUrl),
    );
    request.headers.addAll({
      "Accept": "application/json",
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": "true",
      "Access-Control-Allow-Methods": "GET"
    });

     print(request);
     http.StreamedResponse response = await request.send();
     print(response);
     var decode = jsonDecode(
      await response.stream.bytesToString()
     );
     return decode;
  }

It had nothing to do with the REST API code not containing the CORS functionality, but the actual problem was caused by not using the appropriate request headers. Once I figured that out, I had the solution in my grasp.

JackTheCoder
  • 3
  • 1
  • 3