
In this unit we will focus on how to create a FHIR server.
The objective is that by the end of this unit you will have an idea of what is involved in creating a FHIR server and strategies to include one in your architecture.
Although many people have been working with FHIR for a long time, we know from experience that many times not everyone is aware of what functionalities a FHIR server should or can have and what your obligations are as the creator of a server, so we will review them.
We will present the general concepts for server libraries (how to perform common tasks for FHIR servers: metadata, CRUD methods, etc.) and then describe how each library allows you to do each task.
So, this unit in fact contains 3 different subunits on how to use each library (Java, JavaScript, C#).
These subunits have NO micro-assignments
After Unit 3, you will be able to:
This specific subunit talks about implementing FHIR using Node.JS
Note:
For this unit we used a relatively new (compared with FHIR HAPI) open source package from a company called Asymmetrik.
More details about the product (other than how we used it for this course) can be found:
https://github.com/Asymmetrik/node-fhir-server-core
This product won the Secure Server API Award from the ONC. More details here https://www.hhs.gov/about/news/2018/05/17/secure-api-server-showdown-winner-announced.html
After some years the company was acquired by a company called BlueHalo, and the product has not advanced so much in the last years, although is still being maintained.
Anyway, it's a good example of how to build a facade using node.js. If you want to use it in production beware of the open source product status.
Throughout these sections, you will see three distinct icons:

Sometimes to illustrate a method we use both examples and recipes, and sometimes only examples, depending on the complexity of the subject.
The Node FHIR Server provides several tools for building FHIR servers. It's not specific for a facade, you can also implement every FHIR resource and operation, but it would take a lot of work.
The Node FHIR-Server will take care of:
● HTTP processing and error management
● FHIR RESTful semantics
Node FHIR Server provides a simple way to incorporate the capabilities of a RESTful FHIR server into your applications
The RESTful server is based on middleware, so you can change the specific component for each task by choosing a different component.
The project has good online documentation, so in this document we will focus on the basics and the example project for the course.
The project has two flavors, core and facade.
The core server project, in https://github.com/Asymmetrik/node-fhir-server-core is bare bones and you need only to add one by one your adapter for each resource (as a service module), and register it in the index.js module for the project.
The full facade project, in https://github.com/Asymmetrik/fhir-facade-starter is a full fledged server with all the resources included, almost ready for deploying a US CORE server.
To cover the main functionalities of a server, we used the core server project, and we will focus on seeing how we incorporate the different resources that we want our server to handle and what operations on them we admit.
We will look at the handling of exceptions, the handling of paging and how to expose the capabilities of our server
Let's begin.
Recipe #1: Minimal Project Package for fhir-server-core{
"name": "fhir-server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@asymmetrik/node-fhir-server-core": "^2.2.4",
"axios": "^1.3.1",
"fetch":: "^1.1.0",
"uuid": "^7.0.1"
},
"devDependencies": {
"nodemon": "^2.0.2"
}
}
If we review our bare bones package, the only FHIR specific library we use is
"@asymmetrik/node-fhir-server-core": "^2.2.4"
So the only thing you need to start a FHIR server is include this library in your package.json and type
npm install
The other dependencies are used by our adapters:
UUID: it's the GUID generator for our bundles
fetch: to retrieve information from non FHIR APIs (i.e. our legacy API)
Download our assignment project and get it up and running. You can run it from the terminal doing
nodemon index.js or node index.js
You should see this message:
{"message":"Starting the FHIR Server at localhost:3000","level":"info"}
1- Create the Resource Adapters (services)
2-Add your services to the index.js file
3- Define security and logging
4- Define other middleware options
The first step in enabling a FHIR Server code facade is to define one or more resource services.
Each service is a module with the name <<resourceClass>>.service.js, and will export one or more of the following functions (promises) , related to the corresponding FHIR method:
FHIR RESTful Method | Service Function | Returns |
CREATE | create | new Id (location), error |
UPDATE | update | status |
READ (GET) | searchById | object (resource), error |
SEARCH (BY VERSION) | searchByVersionId | object (resource), error |
DELETE | remove | status |
SEARCH | search | object (bundle), error |
HISTORY SEARCH | history | object (bundle), error |
HISTORY GET | historyById | object (bundle), error |
For example, if you want to allow your server to work with Patient, Observation and Location resources, you will need to generate three resource services, and include for each resource the service functions you need to implement. Example: our legacy system does not support versioning, hence we are not implementing the HISTORY functions. If you are creating a non-versioning, read-only facade for a system, you will need to implement only searchById and search.
Each module must implement one or more methods described in the table above.
For reference, you will find the example in the patient.service.js module in our example project for this course
You will most likely need to add more methods (create, update, operations, etc.).
Currently, fhir-server-core does not support transaction and batch processing.
The Create operation saves a new resource on the server.
The creation function will return the id of the object we created in our legacy system.
As we see in the sample code, it must be declared with the name create, and the resource will come, in FHIR+JSON format (it will fail otherwise) in the req.body.
The following snippet shows how to define a server creation method
Recipe #S-2: Implementing the Create Methodmodule.exports.create = (args, context, logger) => new Promise((resolve, reject) => {
// logger.info('Resource >>> create');
let { base_version } = args;
//The incoming resource is in the request body
//Note: Only JSON is supported
resource = context.req.body;
//Mapping of each resource element
//To our legacy structure
//First we need to extract the information from the incoming resource
let legacyelement1 = resource.element1;
let legacyelement2 = resource.element2;
let legacyelement3 = SpecialMapperForElement3(resource.element3);
...
let legacyelementN = resource.elementN;
//We assemble the object for sequelizer to take
//charge of the instance creation
legacyresource.create({
LEGACY_FIELD_1: legacyelement1,
...
LEGACY_FIELD_N: legacyelementN,
}).then(
legacy => {
//This is the new resource id (server assigned)
newId = legacy.RECORD_ID;
//This is all the information that the response will have about the patient
//the newId in Location
resolve({ id: newId });
})
.else(...if we fail)
});
The SearchById operation retrieves a resource by ID.
It has to: a) Get the information from the legacy system b) Map the information to the desired FHIR resource structure, including vocabulary mappings.
Recipe #S-3: Implementing the SearchById Methodmodule.exports.searchById = (args, context, logger) => new Promise(async (resolve, reject) => {
let { base_version, id } = args;
let baseUrl = GetBaseUrl(context);
legacyPatient
.findOne({
where: { row_id: id },
})
.then(
MyResource => {
if (MyResource) {
R = LegacyToResourceMapper(MyResource);
resolve(R);
} else {
var message = "Patient with id " + id + " not found ";
OO.issue = [{
"severity": "error",
"code": "processing",
"diagnostics": message
}]
resolve(OO);
}
})
.catch(error => {
let OO = new getOperationOutcome();
var message = error;
OO.issue = [{
"severity": "error",
"code": "processing",
"diagnostics": message
}]
resolve(OO);
})
})
The search operation returns a Bundle with zero or many resources, which matches a given set of parameters.
A search is a very powerful and potentially very complicated operation to implement, with many possible parameters and combinations
All the parameters received will come in the args parameter, as seen in the recipe below. You need to create a combined query with all the supported parameters, optimizing the search for your legacy database design.
Defining and implementing this strategy (beyond the trivial combination of parameters) is perhaps the more complex task when implementing a FHIR facade
FHIR defines standardized parameter names for each resource, and these are available from the args object.
The absence of parameters will trigger a query for all available resources, with the default paging.
Note: paging is not defined in this library so you need to create your own. We devised a simple _page=XX, _count=YY schema where XX is which page we want and _YY is how many records per page.
The search method needs to first perform a query with the same criteria counting all the possible records, and then do the actual search with the limit and offset parameters required by paging.
Recipe #S-4: Implementing parameterized search for a resource
module.exports.search = (args, context, logger) => new Promise((resolve, reject) => {
// logger.info('Patient >>> search');
// Common search params, we only support _id
let { base_version, _content, _format, _id, _lastUpdated, _profile, _query, _security, _tag } = args;
// Search Result params ,we only support _count
let { _INCLUDE, _REVINCLUDE, _SORT, _COUNT, _SUMMARY, _ELEMENTS, _CONTAINED, _CONTAINEDTYPED } = args;
let baseUrl = GetBaseUrl(context);
// These are the parameters we can search for,example : name, identifier, family, gender and birthdate
let parameter_1 = args['search_par_1'];
let parameter_2 = args['search_par_2'];
...
let parameter_n = args['search_par_3'];
// Special parameters to support pagination
let coun = context.req.query['_count'];
let page = context.req.query['_page'];
// Special search parameter to search by Id instead of direct read
let idx = args[_id];
let criteria = [];
if (parameter_1) {
criteria.push({ FIELD_1: parameter_1 });
}
if (parameter_2) {
criteria.push({ FIELD_2: parameter_2 });
}
let offset = 0
let limit = 0
if (!coun) { coun = 5; }
if (coun == "") { coun = 5; }
let pageSize = parseInt(coun);
if (!page) { page = 1; }
if (page == "") { page = 1; }
pageInt = parseInt(page);
offset = (pageInt - 1) * pageSize;
limit = pageSize;
//Bundle and Entry definitions
let BundleEntry = getBundleEntry;
let Bundle = getBundle;
//Our Base address
let baseUrl = GetBaseUrl(context);
result = [];
entries = [];
//Get total number of rows
//because we want to know how many records in total we have
//to report that in our searchset bundle
legacy.findAndCountAll({
where: criteria,
}).then(TotalCount => {
//Adjust page offset and limit to the total count
if (offset + limit > TotalCount) {
limit = count;
offset = 0;
}
legacy.findAll({
where: criteria,
limit: limit,
offset: offset
})
.then(
MyLegacyResources => {
MyLegacyResources.forEach(
MyLegacyResource => {
//We map from legacy resource to FHIR
MyResource = LegacyToFHIRMapper(MyLegacyResource);
//And save the result in an array
result.push(MyResource);
});
//With all the resources we have in the result.array
//we assemble the entries
let entries = result.map(resource =>
new BundleEntry({
fullUrl: baseUrl + '/<<ResourceClass>>/' + resource.id,
resource: <<ResourceClass>>
}));
//We assemble the bundle
//With the type, total, entries, id, and meta
let bundle = new Bundle({
id: uuidv4(),
meta: {
lastUpdated: new Date()
},
type: "searchset",
total: TotalCount.count,
entry: entries
});
//And finally, we generate the link element
//self (always), prev (if there is a previous page available)
//next (if there is a next page available)
var OriginalQuery = baseUrl + "<<ResourceClass>>";
var LinkQuery = baseUrl + "<<ResourceClass>>";
var parNum = 0;
var linkParNum = 0;
//This is to reassemble the query
for (var param in context.req.query) {
console.log(param);
console.log(context.req.query[param]);
if (param != "base_version") {
var sep = "&";
parNum = parNum + 1;
if (parNum == 1) { sep = "?"; }
OriginalQuery = OriginalQuery + sep + param + "=" + context.req.query[param];
if ((param != "_page") && (param != "_count")) {
var LinkSep = "&";
linkParNum = linkParNum + 1;
if (linkParNum == 1) { LinkSep = "?"; }
LinkQuery = LinkQuery + LinkSep + param + "=" + context.req.query[param];
}
}
};
//self is always there
MyLinks = [{
relation: "self",
url: OriginalQuery
}];
//prev and next may or not exist
if (pageInt > 1) {
const prevPage = pageInt - 1;
MyLinks.push({
relation: "prev",
url: LinkQuery + "&_count=" + coun + "&_page=" + prevPage.toString()
});
}
MaxPages = (TotalCount.count / coun) + 1;
MaxPages = parseInt(MaxPages);
if (pageInt < MaxPages) {
const nextPage = pageInt + 1;
MyLinks.push({
relation: "next",
url: LinkQuery + "&_count=" + coun + "&_page=" + nextPage.toString()
});
}
bundle.link = MyLinks;
//Now we have all the required elements
//So we can return the complete bundle
resolve(bundle);
});
});
});
Once the resource services are established, the next step is to add them to the server level.
In fhir-server-core each resource or operation you support is called "profile"
What you need to do is to add your resource to the index.js file
Recipe #S-5: Adding a profile to index.jsYou need to add a new item to the profile object, separated from the others with a , for each new resource you are implementing.
, <<resource_class>>: {
service: './<<resource_class.service.js',
versions: [
VERSIONS['4_0_0']
]
}
const { initialize, loggers, constants } = require('@asymmetrik/node-fhir-server-core');
const { VERSIONS } = constants;
//We only support FHIR R4
let config = {
profiles: {
<<resource_class>>: {
service: './<<resource_class.service.js',
versions: [
VERSIONS['4_0_0']
]
}
}
};
let server = initialize(config);
let logger = loggers.get('default');
server.listen(3000, () => {
logger.info('Starting the FHIR Server at localhost:3000');
});
Example #S-1: Adding support for patientlet config = {
profiles: {
patient: {
service: './patient.service.js',
versions: [
VERSIONS['4_0_0']
]
}
}
};
Within RESTful operations, it will generally return resources or bundles, but it will also have to return the errors to the client for various reasons.
By default, fhir-server-core generates error responses for several already established conditions
For example, if the user makes a request for a resource type that does not exist or attempts to perform a search with an invalid parameter, fhir-server-core will automatically generate HTTP 400 Invalid Request and return an OperationOutcome resource in response to the error details.
Similarly, if the implementation of your method throws an exception, the server generally will automatically generate an HTTP 500 Internal Error with an OperationOutcome with details about the exception
By default, fhir-server-core will automatically generate an OperationOutcome resource that contains details about the exception that occurred. We will go through some examples from our project.
Example #S-2: Handling non found resource on get let OO = new getOperationOutcome();
let legacyMapper = LegacyDocumentType;
var mapped = legacyMapper.GetDocumentSystemUse("ID");
var message = "Patient with id " + id.toString() + " not found ";
OO.issue = [{
"severity": "error",
"code": "processing",
"diagnostics": message
}]
resolve(OO);
The Search and History operations return a bundle that contains zero or more resources
RESTful FHIR servers can optionally support paged responses, which means that (for example) if a search returns 500 resources, the server can return a packet containing only the first 20 and a link that will return the next 20, etc.
By default, servers will return all resources immediately in a single bundle
However, sometimes it is not practical to return all the resources if you have thousands of them stored.
There is no built-in paging in fhir-server-core, but you can create your own paging method if it is supported by your underlying infrastructure.
FHIR extended operations ($) are a special profile for fhir-server-core.
To enable an extended operation you need to create the service, and add it to the profiles list
Example #S-3: An extended operation codesystem: {
service: './codesystem.service.js',
versions: [
VERSIONS['4_0_0']
],
operation: [{
name: 'lookup',
route: '/$lookup',
method: 'GET',
reference: 'https://www.hl7.org/fhir/codesystem-operation-lookup.html'
}]
}
In this case we enabled the $lookup on the codesystem resource. You need to implement the service, too with a function named like the service
module.exports.lookup = (args, context, logger) => new Promise((resolve, reject) => {
...Your code here
}
fhir-server-core allows several configuration parameters to customize your server.
Among them are the specific profiles you support, but also:
fhir-server-core authentication is based in passport, so you can include any passport supported strategy
Examples: no authentication (like our project), basic, and bearer
More about passport here: http://www.passportjs.org/
Example #S-3: Configuring basic authentication (not in production please!)
auth: {
strategy: {
name: 'basic',
service: './authentication.service.js'
}
}
Example #S-4: Handler for Basic Authentication
const { BasicStrategy } = require('passport-http');
const jwt = require('jsonwebtoken');
const accessTokenSecret = 'secret_token';
module.exports.strategy = new BasicStrategy((username, password, done) => {
const pass = accessTokenSecret;
if (username === 'accessUser' && password === pass) {
return done(null, {});
} else {
return done(new Error('Invalid Username/Password'));
}
});
This includes corsOptions: for cors management, server host and port, ssl key and certificate storage
, logging log level: as defined by Winston (https://github.com/winstonjs/winston#using-logging-levels)
More details of server configuration here
https://github.com/Asymmetrik/node-fhir-server-core/blob/master/docs/ServerConfiguration.md
The fhir-server-core will automatically export the server capacity declaration by reading the profiles you configured in index.js. Try issuing GET server:/metadata. It's there, and you didn´t program anything about it.
If you need to add your own content (extensions, etc.) to your server's conformity declaration, but still want to take advantage of the automatic generation of a CapabilityStatement, you can extend the built-in generator class.
You can extend the CapabilityStatement declaration by following the instructions here:
https://github.com/Asymmetrik/node-fhir-server-core/blob/master/docs/CustomCapability.md
This week we will have formal "programming" assignments
We will ask you to change or enhance some programs using the fhir-server-core.
FHIR is "just" a specification
In order to do real work, we need to use our desired libraries to enable access for our servers (facade)
We tried with this unit that you grasped common concepts that apply to all FHIR servers and then apply them to the most used platforms / languages (Java, JS , C#)
There are a number of places where you can get information about FHIR
Page