Consuming SOAP Web Services in Node.js with Typescript and soap

Consuming SOAP Web Services in Node.js with Typescript and soap
nodejs
soap web service

Junius L

July 30, 2023

9 min read

SOAP (Simple Object Access Protocol) is a messaging protocol used for exchanging structured information in web services. Node.js, being a powerful and popular server-side JavaScript runtime, offers several libraries for interacting with SOAP-based APIs. In this blog post, we'll explore how to consume SOAP web services in Node.js using the soap library, a widely-used and easy-to-implement solution.

Prerequisites:

Before diving into the tutorial, make sure you have Node.js and npm (Node Package Manager) or yarn installed on your machine. Familiarity with basic JavaScript concepts and working knowledge of web services will be beneficial.

Set up a Node.js Project Let's start by creating a new directory for our project and initializing a Node.js project with yarn:

mkdir soap-web-service-consumer

cd into the directory and run yarn init -y

cd soap-web-service-consumer && yarn init -y

Install the Required Modules

Now, let's install the necessary modules using yarn:

yarn add express cors dotenv ip morgan http-status-codes swagger-ui-express express-openapi-validator swagger-routes-express @apidevtools/swagger-parser

Install TypeScript and Other Development Dependencies

We'll use TypeScript for type checking and better development experience. Additionally, we'll set up nodemon to enable automatic server restart during development.

yarn add @types/cors @types/ip @types/morgan nodemon typescript concurrently -D

Create the Application Structure

Within the soap-web-service-consumer directory, create an app folder and an app.ts file inside it. This file will contain the code for our Express application.

mkdir app && touch app/app.ts

Build the Express App

Inside app.ts, let's build the Express app:

import express, { Application, Request, Response } from 'express';
import cors from 'cors'
import morgan from 'morgan';
 
export const App = async(): Promise<Application> => {
  const app: Application = express();
 
  app.use(express.json())
  app.use(express.urlencoded({ extended: false}))
  app.use(morgan('tiny'))
  app.use(cors())
 
  app.get('/', (req: Request, res: Response) => {
    res.send('API is up and running')
  })
 
  return app;
}

In the root directory create an index.ts file and import the App we created in previous step

touch index.ts
import 'dotenv/config'
import IP from 'ip'
import { App } from './app/app'
 
const PORT = process.env.PORT
 
App()
  .then(app => app.listen(PORT))
  .then(() => console.log(`App is running on http://${IP.address()}:${PORT}`))
  .catch(e => {
    console.log('App is failed start', e);
 
  })

Configure TypeScript and Create Start Scripts

In the root directory of your project, create a tsconfig.json file to configure TypeScript:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "rootDir": "./app"
  },
  "include": ["./app/**/*.ts"]
}

Next, open package.json and add the following scripts:

"scripts": {
  "build": "tsc",
  "start": "node dist/index.js",
  "dev": "concurrently \"tsc --watch\" \"nodemon --ignore ./dist/ -q dist/index.js\""
},
 

Let's go over our script

build - will be used to build our application and convert Typescript to JavaScript

start - starts the application

dev - is used for dev mode, this will watch our files for any changes and auto restart the server, if there's any changes

Let's start our application by running the following command

yarn dev

Open your browser and navigate to http://localhost:8080 or http://YOUR_IP_ADDRESS:8080

You should see API is up and running message displayed

Consuming the SOAP Service

Now, let's create a SOAP client to consume the web service. We'll create a clients directory inside the app folder and add the calculator.ts file and wsdl directory to it:

mkdir app/clients
mkdir -p app/clients/calculator/wsdl
touch app/clients/calculator.ts

head over to http://www.dneonline.com/calculator.asmx?wsdl and inspect the wsdl we are going to use in the project.

Download the wsdl from http://www.dneonline.com/calculator.asmx?wsdl and save it inside the wsdl directory we created earlier.

Paste the following code inside calculator.ts file

import {createClientAsync} from 'soap'
import path from 'path'
 
interface IParams {
  intA: number;
  intB: number;
}
 
const wsdl = path.resolve(__dirname, './wsdl/dneonline.com_calculator.asmx_wsdl.xml');
const url = 'http://www.dneonline.com/calculator.asmx?wsdl'
 
const setupClient = async() => {
  return createClientAsync(wsdl, {}).then((client) => {
    client.setEndpoint(url)
    client.addSoapHeader({ 'soap:Connection': 'keep-alive'})
 
    return client
  })
}
 
 
export const addition =  (params: IParams) => setupClient().then(client => client.AddAsync(params))

Let's go over the code, the first two lines import both soap and path modules.

Here we create an interface named IParams which is the parameters that the soap service expects

interface IParams {
  intA: number;
  intB: number;
}

Then we reference the wsdl we downloaded earlier using the path module and set the url, which points to our web service

const wsdl = path.resolve(__dirname, './wsdl/dneonline.com_calculator.asmx_wsdl.xml');
const url = 'http://www.dneonline.com/calculator.asmx?wsdl'

And setup our client, which we will use to call the methods in the soap service. The soap service has four methods Add, Subtract, Multiply and Divide.

const setupClient = async() => {
  return createClientAsync(wsdl, {}).then((client) => {
    client.setEndpoint(url)
    client.addSoapHeader({ 'soap:Connection': 'keep-alive'})
 
    return client
  })
}

and the last line calls the Add method inside the soap service, we are using the client above to call Add method

export const addition =  (params: IParams) => setupClient().then(client => client.AddAsync(params))

The setupClient function returns a promise which has the four mentioned methods/function. Then we can access those functions, using the dot notation for example client.AddAsync(1, 3), client.MultiplyAsync(2, 2)

Create a Controller

Now, let's create a controller to handle the incoming requests and use the SOAP client to call the calculator's Add method. Create a controllers directory inside the app folder and add the calculator.ts file to it:

mkdir app/controllers && touch app/controllers/calculator.ts

Paste the following code into calculator.ts:

import { Request, Response } from 'express';
 
import * as calculator from '../clients/calculator/calculator'
import { errorResponse, successResponse } from '../utils/helpers';
 
export const addition = async(req: Request, res: Response) => {
  try {
 
  const response = await calculator.addition({ intA: req.body.intA, intB: req.body.intB})
 
  successResponse(res, `Successfully added ${req.body.intA} and ${req.body.intB}`, response)
 
  } catch (error) {
    errorResponse(res, error)
  }
}

This's a simple controller, which imports the calculator client and call the addition function. It also import two functions from util helpers, which we'll create next.

In the app directory, create a new directory called utils and a file called helpers.ts

mkdir app/utils && touch app/utils/helpers.ts

and paste the following code.

import { Response } from 'express';
import { StatusCodes } from 'http-status-codes';
 
export const successResponse = (res: Response, msg: string, data: [] ) => {
 
  res.status(StatusCodes.OK)
  res.json({
    success: true,
    error: null,
    message: msg,
    // @ts-ignore
    body: data?.[0]
  })
 
}
 
export const errorResponse = (res: Response, error: any ) => {
 
  const message = error.message || 'Something went wrong. Please try again later'
 
  res.status(error.statusCode || error.code || StatusCodes.INTERNAL_SERVER_ERROR)
  res.json({
    success: true,
    error: error,
    message: message,
    body: null
  })
 
}

Let's add route to our app/app.ts

import express, { Application, Request, Response } from 'express';
import cors from 'cors'
import morgan from 'morgan';
import { addition } from './controllers/calculator'; // imports the calculator controller
 
export const App = async(): Promise<Application> => {
  const app: Application = express();
 
  app.use(express.json())
  app.use(express.urlencoded({ extended: false}))
  app.use(morgan('tiny'))
  app.use(cors())
 
  // added new routes
  app.post('/addition', addition);
 
  app.get('/', (req: Request, res: Response) => {
    res.send('API is up and running')
  })
 
  return app;
}

Let's run the application and test it using curl or postman

curl --location 'http://localhost:8080/addition' \
--header 'Content-Type: application/json' \
--data '{
    "intA": 2,
    "intB": 1
}'

We get the following response

{
  "success": true,
  "error": null,
  "message": "Successfully added 2 and 1",
  "body": {
      "AddResult": 3
  }
}

That's it.

Define Swagger Schema (Bonus)

Now, let's create a routes directory inside the app folder and add an index.ts file to it:

mkdir app/routes && touch app/routes/index.ts

Paste the following code into index.ts:

// app/routes/index.ts
import * as calculator from '../controllers/calculator';
 
export const routes = {
  ...calculator
};
 

Adding swagger to our api for docuemtation

Now, create the swagger.yml file inside the app/swagger directory:

mkdir app/swagger && touch app/swagger/swagger.yml

and paste the following definitions

openapi: 3.0.3
info:
  title: Calculator API
  version: '1.0.0'
 
servers:
  - url: '/api/v1'
 
paths:
  /addition:
    post:
      summary: Performs addition on two numbers
      operationId: addition
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Addition'
      responses:
        200:
         description: OK
         content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddResponse'
 
components:
  schemas:
    AddResponse:
      type: object
      properties:
        success:
          type: boolean
        error:
          type: string
        message:
          type: string
        body:
          type: object
          properties:
            addResult:
              type: number
    Addition:
      type: object
      required:
        - intA
        - intB
      properties:
        intA:
          type: number
        intB:
          type: number

In the above schema we define one addition path /addition which uses a post method and accept two parameters, defined in the component section of the schema

Addition:
  type: object
  required:
    - intA
    - intB
  properties:
    intA:
      type: number
    intB:
      type: number

and the response

AddResponse:
  type: object
  properties:
    success:
      type: boolean
    error:
      type: string
    message:
      type: string
    body:
      type: object
      properties:
        addResult:
          type: number

To learn more about this visit Open Api

Connect the Routes to Swagger

In app.ts, update the code to connect the routes to Swagger and use express-openapi-validator middleware:

import express, { Application, Request, Response } from 'express';
import SwaggerParser from '@apidevtools/swagger-parser';
import {connector} from 'swagger-routes-express';
import * as OpenAPiValidator from 'express-openapi-validator'
import swaggerUI from 'swagger-ui-express'
import cors from 'cors'
import morgan from 'morgan';
import { routes } from './routes';
 
export const App = async(): Promise<Application> => {
 
  const apiDescription = await SwaggerParser.validate('app/swagger/swagger.yml')
 
  const connect = connector(routes, apiDescription)
  const app: Application = express();
 
  app.use(express.json())
  app.use(express.urlencoded({ extended: false}))
  app.use(morgan('tiny'))
  app.use(cors())
  app.use(OpenAPiValidator.middleware({
    apiSpec: 'app/swagger/swagger.yml'
  }))
 
  app.get('/', (req: Request, res: Response) => {
    res.send('API is up and running')
  })
 
  app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(apiDescription))
 
  connect(app)
 
  return app;
}

Now you can access the Swagger UI documentation at http://localhost:8081/api-docs/ and test the API's addition endpoint. Additionally, you can use tools like curl or Postman to make requests to the /addition endpoint and see the results.

Challenge

Add the remaining functions, Subtract, Multiply and Divide.