Creating an articles service
Let's build a service to provide our blog page with articles from markdown files
Created at:
Last updated:
Table of Contents
Introduction
Previously (See here) we focused our attention on preparing tools which now are going to make sure that the code we produce is consistent.
But now it is time to start building actual functionality for our website. We will start by creating a service which will provide us with articles. Those articles are going to be written in Markdown as I mentioned in Getting Started. Markdown is really easy to use and provides a lot of different styles and formatting to our text, storing everything in simple text files. Thanks to that we have a lightweight and easy to use way of storing our articles. But let's jump into code.
As before I am creating a separate branch to work on this new feature.
Markdown files
We of course are going to store everything just in plain text files, but we would also like to have some metadata for each of our articles. I am thinking about these properties:
These are the basic information which we are going to use.
This information we will store as a front-matter, which is basically YAML formatted data placed at the beginning of each article file. For example:
Files structure
So, let me describe here how I want to structure the code and the data.
- All our articles we are going to store in the folder in the root directory. Inside this folder we can create new folders to store related articles.
- All services and in general code not related to web pages and visual components we are going to keep inside .
- I want to keep our code clean and possibly decoupled, so we are going to create interfaces and make our services implement those. In the future if for whatever reason we might want to replace our service which reads articles from files with one which reads them from database, we will just implement the same interface in then new service and replace the old one without a need to replace any methods or anything more. To achieve this we will have and directories in for our interfaces and implementations respectively.
Creating interfaces
Building our articles service we will start by creating some interfaces. Why interfaces? Because we aren't going to put everything into one class and call it a day. We are going to create more than one service to keep related functionalities separate from each other. We are going to have two interfaces for now - which will transform markdown files content into objects and which will, as the name may suggest, take care of reading files.
So in we create these files:
Interface
We want our service to read files and directories tree for us, so we can then use this data to group articles. We also want to simply read file content, so we can later use it in . I also already know we will need method for reading file stats.
Our looks like so
We define our interface and some types describing The structure of data.
Interface
Here we need a method to get all our articles and a single article based on the slug. Also, we will need a method to get articles paginated, so we don't have to list them all on a single page.
This is our
As you can see we have some imports from . I created this file in the folder to keep there some common types which are not strictly connected to the article service but will also be used in other places.
Normally type declarations, and in general any functions, variables and so on, should be declared as close to their scope as possible. This indicates in which scope the function or anything else we declare is used.
Exports in
Before I show you what do we have in let's finalize the with the . We are just doing a few exports here.
Why do we even have this file? Well, the answer is, to make imports in other files later on more organized. If we left just the exports in, for example, and , we will have to import it in other places like so
This maybe looks okay but imagine now having to import 20 different services, we would end up with 20 lines of imports on the top of our file. We also don't need to know exactly where is each service coming from. With the file the same imports look like this instead:
Much cleaner, huh?
Shared types in
So what types do we want to declare here? What do we need? For sure, we have to declare how our object will look like. It will be built from and actual article . The will contain all the properties other than the article content itself which are coming mostly from . The last type just describes the front-matter properties we decided to put in each article.
The in our is a kind of ID we are going to use to identify each article and to use in browser to navigate to the article. refers to the name of the sub-folder we might want to create to keep similar articles together. is either the from front-matter or the file modification date if is not present for some reason and then transformed to the object.
One interesting thing here as well is the type which is using a generic type to describe the . This way we can use it basically for any kind of object, not only articles.
Implementing files service
Our articles service will need a way to read files, so we have to start with the files service first. We simply call this service . So we are creating folder inside with and inside.
Building class
We are going to use classes to provide encapsulation and also to be able to implement interfaces.
In our we declare our class structure.
This is the skeleton of our class. Now we are going to build each method.
Let's start with the hardest part, as this function will be recursive and will traverse the files tree in a given directory, giving us back description of that tree in form of an array of containing nested files and directories.
This can be done with the following code:
Here we pass the directory which we want to build the files tree for as a parameter. Then we read all the files and directories within the given directory, and we loop through them. If the element is a file we simply save it in the resulting array as a object. In case we encountered a directory we build the object. To get the directory children, we are calling the again, passing path to the directory we are currently building. This approach builds a nested tree structure.
Now it's time to implement the method which will provide us with information about the file or a directory. We are starting with this method, because will make use of it.
The function implementation looks like this:
First it makes sure that the path we provided is pointing to something that exists, then it reads the stats of the directory/file on that path.
Our next target is a function which simply reads the content of a file ion a given path. To do so it first checks if the path provided as a parameter points to a file, and then it simply reads the file content in an utf8 format.
Helper methods
We are now left with the last three methods in our class. These are , and . There are just basic types checkers which we can use for example to filter out elements we are looking for.
They all are using the type guards to predicate the type of the parameter passed to the function.
To finish this part, we need to add exports to the and file.
Implementing articles service
Now with our in place, we can move to the part most of you were waiting for.
We are going to start the implementation by creating necessary files. I am going to name this service to give some explanation what it actually does. We are creating folder inside . Inside we add and .
Building class
As with the previous service, let's first declare a class which implements the proper interface we've written a bit earlier.
Let's now fill this scaffold with code.
First we can take a look at the way of getting and returning all articles. To do this we will make use of the to make the files tree and then read all article files. Then each file content we are going to transform into and object, sort by date and return them as an array.
Body of this function is simple and easy to read and understand, as we moved some responsibilities here, to separate private functions.
Now we take a look at . This function requires passing path to the directory which we want to get articles from. It uses the to fetch all articles n that directory. These are files that are in some sub-folders and those which are just directly in the directory we are checking. All the files re are associating with the folders they are part of, and we return the array objects with and properties.
For those articles which are not in any sub-folder, the is defined as a dummy object without any name. We just give it the absolute path.
It is also worth noticing that we are only reading articles which are one sub-folder deep in the path we are checking. We for sure could find a way to make it recursively walk through any depth but for simplicity, this time we don't want our topics (sub-folders) to have more nested topics inside.
Form now on our class has a constructor which takes the directory and a file service as parameters. Why do we pass file service here instead of creating new in the constructor? Well we don't want to couple our article service to the concrete implementation of the files service. We just want the provided files service to implement the interface we defined at the beginning. This method is called dependency inversion.
This is the mapper function which uses the object as a parameter and transforms it to the object we described in .
Here we are using to get the topic. We also use file contents to get all the parameters of an object. We are also getting stats of the article file, and we use the file modification date in case the front-matter is missing the field.
String service
If you look carefully we are introducing here new . It is a class with just two static methods, one to join array of string into one string and the other to transform a string into slug. As this service does not use any interface (because interfaces in TypeScript cannot define static methods) we didn't create it earlier. We also didn't create the service itself yet, as the idea of abstracting these functionalities into separate class came to my mind during the implementation.
So we just add a new directory in with and inside. That's how these two files look like:
We also add to the same way we did for all the other services before.
Another new thing is the function used to get the front-matter data from the file. This comes from the package which we install with this command in our terminal:
After successfully implementing a method to get all the posts, the next one is to get just one post. This method gets the article's slug as a parameter and uses it to find the article we are looking for.
We are simply getting all articles and try to find the one with the given slug. This approach is for sure not the most optimal one as it has to find all the articles and map them to objects before we try to find the one we need, but this service will only run during build of our blog as the whole blog will have only static pages.
And finally the last part which is the method. This method is responsible for giving us back just those articles which fulfill the requested page options, as well us information about the current page.
Again, we fetch all articles, then we take only these articles which are on the page we are requesting. We also return flags informing if there is a previous or next page and total pages count.
Exporting the finished service
To finish this part, we need to export our service. To do so we add the export file.
And the same we do in .
On the screenshot below, you can see how the files structure looks like now.
Before we make use of this service to show a list of all articles and open one selected article, we should commit our changes.
Create a page to list all articles
Now we are ready to make use of the article service. To do so we first need some articles. You can write some, but you can also use those I've generated. You can find them in our projects repo with this commit. We will keep these files in our project's root directory in .
Next, we create folder inside and a file inside.
In this file we declare a function which will render our articles.
This as you might notice is making an import from which we don't have yet. Lets's create file in directory and declare the inside.
This constant is pointing to the place where we keep our articles.
We can now run in the terminal and navigate to to see the list of articles we've created.
Now it's time to commit the changes and move to the last part.
Create a page to show individual article
Now in the directory we can create our page. To do so create directory in and inside this folder the file. The directory works as a dynamic route and the parameter will be provided to our page component so based on that we can fetch the article we need and render it on the page.
Please notice the function here. This function is generating all the parameters the can take. This means that based on that we know what we can expect and all the other values we can handle in other ways. In our case we are setting to false what means that if we go to any that we don't have an article for, we will redirect user to 404 page.
Now if we start our development server with and go to we can see our article rendered on the page. Maybe it is ugly but at least we know it works as intended.
One extra thing we can do also here before we finish this long article is to add a link component around each of our articles in the , to allow user to navigate to an article by clicking on it.
Here is the updated in
We are importing component from which basically works as an anchor tag. We also have to declare the constant in
Now if we go to we can click on each of our articles to open it.
Not it's time to do the final commit.
After that, as described in Linting and formatting we open the pull request and merge it back to the main branch.
And that's all in this a bit long article. I hope you enjoyed it and see you in the next one where we are going to look at unit tests setup. Setting up unit testing