expresshttp-status-code-404verceljson-server

json-server with express api with requests to external api - gets 404 error responses


I am developing an interface api that receives a request from a client to that external api, and redirects it to it.

That api has a database file (userData.json) to store the users data, including their api keys.

The api has the routes to the external api, and only one more route to create a new user from the interface api (createUser). This route receives the request to create a new user (createUser) that will generate a unique user api key, and store all the user data, including this key, in an array in userData.json.

Locally, I managed to create that api with success. But when I tried to put it to work in Vercel, I realized Vercel doesn't allow to change existing files from serverless functions.

Then I came up to this article, that explains how to do this with json-server: https://ivo-culic.medium.com/create-restful-api-with-json-server-and-deploy-it-to-vercel-d56061c1157a

To adapt this example to my case, I have put the app requests to the external api (express) inside a server.use() (json-server).

When I start the api with node I am able to redirect the requests to the external api, but receive a 404 for any json-server request.

When I start the api with json server, I am able to go through with json-server requests, but not with the external api requests (app).

I am still working locally, so it has nothing to do with Vercel.

How can I make it go through with both types of requests at the same time?

This is the code of the server.js file. I just left one route to the external api as an example:

// Import Express.js
import http from 'https' 
import express from 'express'
import jsonServer from 'json-server'
import path from 'path'

import {authenticateKey, createUser} from './api/apiAuth.js'
import {ixApi} from './ixApi.js' 

// This variable defines the port of your computer where the API will be available
const PORT = 3000

// This variable instantiate the Express.js library
const app = express()

app.use(express.json()); 
app.use(                
  express.urlencoded({
    extended: true,
  })
);  

// JSON Server module
const server = jsonServer.create();
const router = jsonServer.router("db.json");

//const router = jsonServer.router(path.join("", 'db.json'))

// Make sure to use the default middleware
const middlewares = jsonServer.defaults();

server.use(middlewares);

// Add this before server.use(router)
server.use(

  /**
   * Welcome message
   */
  app.get('/', (request, response) => {
    // The string we want to display on http://localhost:3000
    response.send('Welcome on the ix interface API!')
  }),

  /**
   * @function create-user - Creates new user
   */
  app.post('/create-user', authenticateKey, async (req, res) => {
    //create a new user with "user:Username"
    let username = req.body.username
    let appName = req.body.appName
    let userEmail = req.body.userEmail
    let IX_ACCOUNT_NAME = req.body.IX_ACCOUNT_NAME
    let IX_API_KEY = req.body.IX_API_KEY
    let user = await createUser(username, appName, userEmail, IX_ACCOUNT_NAME, IX_API_KEY, req, res)

    console.log('USER 1:')
    console.log(user)

    res.status(201).send({ data: user });
  }),


  /**
   * @function createInvoice - Creates invoice
   *
   * CALLED BY:
   * - 
   *
   * TO IMPROVE:
   * -
   */ 
  app.post('/create-invoice', authenticateKey, async (request, response) => {
    console.log(response.locals.ixCredentials)  
    console.log(request.body)
    console.log(request.body.invoiceData)

    const IX_ACCOUNT_NAME = response.locals.ixCredentials.IX_ACCOUNT_NAME
    const IX_API_KEY =  response.locals.ixCredentials.IX_API_KEY
    const invoiceData = request.body.invoiceData
    const items = request.body.invoiceData.items

    const creationRequest = () => {
      const response = new Promise ((resolve) => {
        var options = {
          "method": "POST",
          "hostname": IX_ACCOUNT_NAME + ".app.ix.com",
          "port": null,
          "path": "/invoices.json?api_key=" + IX_API_KEY,
          "headers": {
            "accept": "application/json",
            "content-type": "application/json"
          }
        };

        var req = http.request(options, function (res) {
          var chunks = [];

          res.on("data", function (chunk) {
            console.log('CHUNK')
            console.log(chunk.toString)
            chunks.push(chunk);
          });

          res.on("end", function () {
            var body = Buffer.concat(chunks);

            console.log('IX BODY:')
            console.log(body.toString());

            resolve(body.toString())
          });

          console.log(res.statusCode)

        })

        var itemsList = []                
        
        for (let i = 0; i < items.length; i++) {
          itemsList.push({ 
            name: items[i].name,
            description: items[i].description,
            unit_price: items[i].price,
            quantity: items[i].quantity,
          })

          console.log(itemsList)
        }

        req.write(JSON.stringify({ invoice: {
          date: invoiceData.date,
          due_date: invoiceData.due_date,
          client: {
              name: invoiceData.client.name,
              email: invoiceData.client.email,
              country: invoiceData.client.country,                        
              fiscal_id: invoiceData.client.vat,                        
              code: invoiceData.client.vat,
          },
          items: itemsList,
        }}));                                     

        req.on('error', function(err) {
          console.log('IX ERROR:')
          console.log(err)

          //return response.json({ success: false })                    
        });

        console.log('SENDING REQUEST:')  

        req.end();                  
      })

      console.log('RESPONSE')
      console.log(response)

      return response

    }

    const returnedData = await creationRequest()
      .then (response => { 
        console.log('INVOICE CREATED')     

        var obj = JSON.parse(response) 

        console.log(obj)     

        return obj
      });

    console.log('RETURNED DATA')
    console.log(returnedData)   

    return response.json(returnedData)     
  })
)

server.use(
    jsonServer.rewriter({
    "/api/*": "/$1",
  })
)

server.use(router);

/**
 * The code below starts the API with these parameters:
 * 1 - The PORT where your API will be available
 * 2 - The callback function (function to call) when your API is ready
 */
server.listen(PORT, () =>
  console.log(`The ix API is running on: http://localhost:${PORT}.`)
)

// Export the Server API
export {server}

I tried to use the middleware function of json-server to solve the issue, even though I am not sure if this function is appropriate for that, and found no success.

I would be grateful for your help, since I am for several days now trying to find a solution.

Thanks!


Solution

  • The issue is caused by the middleware created by jsonServer.defaults([options]). Remove following statements will work:

    // const middlewares = jsonServer.defaults({
    //   static: undefined,
    // });
    
    // server.use(middlewares);
    

    For some reason, the json-server publish the public directory with index.html file in their package. See directory structure of node_modules/json-server:

    ☁  json-server [master] ⚡  tree -L 2 -I 'node_modules'
    .
    ├── LICENSE
    ├── README.md
    ├── lib
    │   ├── cli
    │   └── server
    ├── package.json
    └── public
        ├── favicon.ico
        ├── index.html
        ├── script.js
        └── style.css
    
    4 directories, 7 files
    

    When you use the middlewares created by jsonServer.defaults([options]), it will serve static files, see v0.17.3/src/server/defaults.js#L35

    const defaultDir = path.join(__dirname, '../../public');
    // ...
    arr.push(express.static(opts.static))
    

    The default static file directory is public, so when you access the http://localhost:3000/, the public/index.html document will be returned to the client instead of hitting the API endpoint.

    A working example for both JSON server API and custom routes.

    import express, { Router } from 'express';
    import jsonServer from 'json-server';
    import path from 'path';
    
    const PORT = 3000;
    const app = express();
    
    app.use(express.json());
    app.use(
      express.urlencoded({
        extended: true,
      })
    );
    
    const server = jsonServer.create();
    const router = jsonServer.router(path.join(__dirname, 'db.json'));
    // const middlewares = jsonServer.defaults({
    //   static: undefined,
    // });
    
    // server.use(middlewares);
    
    const customRouter = Router();
    customRouter.get('/', (req, res) => {
      res.send('Welcome on the ix interface API');
    });
    customRouter.get('/create-user', (req, res) => {
      res.status(201).json({ data: { name: 'teresa teng' } });
    })
    server.use(customRouter);
    
    server.use(
      jsonServer.rewriter({
        "/api/*": "/$1",
      })
    )
    server.use(router);
    
    server.listen(PORT, () => {
      console.log('JSON Server is running')
    });
    

    db.json:

    {
      "posts": [
        { "id": 1, "title": "json-server", "author": "typicode" }
      ],
      "comments": [
        { "id": 1, "body": "some comment", "postId": 1 }
      ],
      "profile": { "name": "typicode" }
    }
    

    Testing custom routes:

    ⚡  curl -X GET http://localhost:3000
    Welcome on the ix interface API% 
    ⚡  curl -X GET http://localhost:3000/create-user
    {
      "data": {
        "name": "teresa teng"
      }
    }%  
    

    Testing JSON server API:

    ⚡  curl -X GET http://localhost:3000/api/posts/1
    {
      "id": 1,
      "title": "json-server",
      "author": "typicode"
    }%