It was early. I was one of the first ones in the office. I made a coffee and I sat down at my work station. I opened Jira to pull a new story into progress. I opened the IDE and cloned the project. I was delighted to see it was using TypeScript. This feeling was going to change very soon.
We had only a few unit tests that were mocking almost everything. They were not the most useful tests. I added a test for my change. I committed my changes and pushed them to the code repo.
All in all, the project was not as bad as it might seem. It could have been improved though. I wanted to do it but I had never built a setup for express with TypeScript before. This was going to change in the following week. What follows is the story of how I did it and what I learned along the way.
The obvious ones
I knew what the first step must be. I needed to initialize the project
npm init -y
This was simple enough. What now? Well, it was obvious that I needed to install Typescript and configure it. So let's just do that
npm install --save-dev typescript
Let's see if this works.
The command executed successfully so it seemed to work.
Let's initialize the TypeScript project now.
npx tsc -init
This is creating a file called tsconfig.json. It contains the settings for TypeScript. I uncommented the line that contained the key outDir and I made sure its value was set to ./dist
I was planning to have the source files in the src directory. After a bit of research, I figured I had to add an include setting to tsconfig.json. I modified the file and I added the following field just after the compilerOptions.
I wanted to be able to debug TypeScript files so I had to uncomment the line that contained the sourceMap field as well
It seemed I had figured out the TypeScript part. Next was the other obvious thing. I had to setup express.
A lot of questions were popping in my head at this time. How am I going to make express work with the type system from TypeScript? Can I write controllers as classes? I want to have services that contain my business logic. How am I going to add them as dependencies for the controllers? How about testing? What are the options there? Soon I learned the answer to all of these questions.
I started by installing express
npm install --save express
I wanted to make the project work with the type system from TypeScript. I just had to install the types for express and node
npm install --save-dev @types/express npm install --save-dev @types/node
I now had to add scripts to the package.json for compiling TypeScript files and starting the project
As you can see the start script is calling the compile script first. No surprises when someone starts the project for the first time.
So far I was happy to see the pieces of the puzzle falling into place. How about when developing on the project? Do I want to restart the project every time I make a change? It would be cool to add nodemon to have it watch over my changes and restart the project automatically for me.
I installed nodemon for that
npm install --save-dev nodemon
Then I had to add a script to the package.json for starting the project for development
It's always cool to play by the rules of the language. When you start out it's quite hard to get everything right. The IDE is helping a lot in this respect but you can't make sure everyone has the same IDE setup. This is where I see linting to be useful. This was a nice to have though. I think these nice to haves are always welcome on projects so I've added linting as well.
I installed tslint
npm install --save-dev tslint
I created the tslint.json for configuring the linter
Then I added a lint command to package.json
Testing is one of the most critical parts of the development process. Not only it makes sure your code works, but it also gives you peace of mind while refactoring. It ensures that your code has a better structure as well. All of these add a lot to the code quality. I wanted to have multiple types of tests. I usually write unit, integration, and functional tests. Most of my tests being integration tests.
A good option for testing these days seems to be jest. This is also working with TypeScript. I was in luck. I installed it toghether with the TypeScript types and the jest TypeScript preprocessor ts-jest
npm install --save-dev jest npm install --save-dev @types/jest npm install --save-dev ts-jest
Next I had to configure jest.
This allowed me to have the source files in the src directory and the test files in the tests directory. I also added the transform setting for calling the preprocessor for TypeScript files. The ts-jest module also has support for source maps which allowed me to use the debugger when writing tests.
Then I added the commands for testing to the scripts in package.json
As you can see, jest comes with test coverage support which is super cool. It's always nice having a look at the coverage numbers before committing my work.
This is covering the unit and integration part of my testing. How about the functional tests? That's where supertest enters the picture. This module can send HTTP requests to your application from the test environment. Let's install this as well then
npm install --save-dev supertest @types/supertest
Taking it for a spin
Now that I had the setup pretty much figured out, I needed to see what the experience of developing something using this setup felt like. I planned to create a toy of an api that would return a greeting.
I wanted to start by writing something simple. I did not handle the express setup first because I knew that had the potential to change after writing the controller and the service. I started with the service
There is nothing easier than this. Let's write a unit test for it
This is testing that the service can be instantiated and that the method is returning the right greeting.
Next thing on the list was adding a controller that would call the greeting service
Cool, but how was I going to pass the GreetingService dependency to this controller? I didn't want to instantiate the service myself and then pass it when I instantiate the controller. I wasn't even considering instantiating the service in the controller. This is something that dependency injection should do for me. After a bit of research, I found a module called typedi that does dependency injection for TypeScript. I installed it and its reflect-metadata dependency
npm install typedi --save npm install reflect-metadata --save
Then I had to enable the following settings in tsconfig.json
This is enabling decorators for the project. The dependency injection module needs this. Little did I know that later on, I needed the decorators for the controllers as well.
While researching the dependency injection I stumbled upon a very cool module for routing requests to controllers in TypeScript with express that is using typedi. I installed it together with all its dependencies
npm install --save routing-controllers npm install --save body-parser multer npm install --save-dev @types/body-parser @types/multer npm install --save class-transformer class-validator
Then I refactored the home controller to use the appropriate decorators from routing-controllers
This is declaring the HomeController as being a Controller. It also makes the index method be called when a GET request to / is sent to the api.
I now had to tackle the express integration. I had to create the express app so that I could pass it to routing-controllers
The line where the reflect-metadata is imported is essential. Without it, the dependency injection and the decorators for the routing-controllers would not work. Another important aspect is that I had to register the container class to be used by the routing-controllers.
Now that I created the app I could make it listen on port 3000
In an ideal world, I would have tested the controller before I did all these. Somehow I found it easier this way though.
Speaking about tests, I thought a good option to test the dependency injection was to start with the service again so I created an integration test for that
Well, it did not work. I figured it must be from the reflect-metadata not being imported for tests. I remembered afterward that I also had to register the container for tests. I wanted to have this on a global level and not import it in every test. I added a jest setup file to jest config
In the global setup file I imported the reflect-metadata and I registered the container in the routing-controllers for tests as well
This time the service integration test passed. Then I though it would be cool to add an integration test for the controller as well
As you can see I get the HomeController from the dependency injection container. This allows me not to be worried about its dependencies and potentially instantiating those as well.
How about a functional test? I would like to send an HTTP request to the api to see that it works. This is where supertest enters the scene
I'm doing a GET request to / and expect that I would get "Welcome home!". Well, it works.
I am quite pleased with the result and with the development experience. I will undoubtedly use this setup for a side project or at work. There are some other aspects that I would like to experiment with. I would like to see how this works with a persistence layer for example. I will let you know how that went when I get to it.
See you soon
This was my adventure creating a setup for express with TypeScript. If you want to see the final version of it, you can find it at https://github.com/thelexned/typescript-express