This book walks you through building a web application for managing a shop. The backend will be written using the Phoenix framework and the frontend will be built using VueJS.
We will alternate between writing the backend and the frontend as we progress. We will also talk about the the deployment process.
The source code for client is available here and for server it is available here.
Each chapter will be a single commit in the repository, so that it can be easily checked out and followed along as we progress.
Rough Overview
Before starting programming, we should make a rough architecture of our project.
The backend will communicate with the frontend using a REST API. The functionality offered by the project will be as follows.
Support for inventory management which includes keeping track of products, the stocks, the taxes and more.
Support for billing where a customer can buy multiple products.
Transportation service for goods bought by the customer.
More requirements in the future.
Database Structure modeled using Typescript Interfaces
Lets simply model these requirements into database tables along with the Typescript interfaces.
This provides us with a structure/model, which can be used to communicate between frontend and backend.
It also makes our models standard and easier to understand. Soon we will also learn how to automatically generate such an interface from the database tables from phoenix framework.
Here are typescript interfaces for the models we will start with.
In the next chapter, we will see how setup the phoenix framework for our backend and model the database tables. It will have the same structure as the typescript interfaces we just designed.
Setting up Your Phoenix Application
Setting up Phoenix with PostgreSQL
Phoenix docs provides the complete tutorial to install elixir and phoenix framework. Go ahead and install it, I will wait.
Once we have installed it, lets start a new phoenix project. Mix is the build tool that ships with Elixir. It provides tasks for performing various operations. Lets use mix to create a new project.
We will name our project ms(Yeah why not :D).
This will walk you through creating the project and installing the dependencies. Once it is finished, we will get back the prompt. Now lets import the generated project in to our IDE.
I use the Intellij IDEA along with intellij-elixir plugin. You can also use other editors.
Import the project via File > Open > path to ms folder, where the mix.exs file is present. Once the project is imported, we look at the database connectivity.
In order to know our database credentials we go ms/config/dev.exs.
We need to setup our PostgreSQL with these credentials. Lets set it up using docker and PostgreSQL. Go ahead and install docker from here.
Now we pull the PostgreSQL container and run it using the below command.
As we see we pass the PostgreSQL username, password and database as environment variable to docker using -e flag.
Now we need to create the ms_dev database and run the ecto migrations. For this, run the following commands.
Now your phoenix app is reachable at localhost:4000. At this point onwards, I expect a basic understanding of Elixir.
For a quick overview, please read elixir in Y minutes.
Other Phoenix / Elixir concepts will be explained as we progress. In the next chapter we will model and create our database tables using ecto.
Setting up a PostgreSQL backed HTTP server
Generate tables using Ecto
It is time to make our models and tables using Ecto. Ecto is the library phoenix offers for working with databases. It includes a DSL in which we will be writing our queries. Ecto queries are composable and prevent SQL injections. Lets start with the Products table. If you are unsure how the table structure should look like, refer previous chapters.
The phx.gen.json task generates the migrations, tables/models, the view and the controllers. The views generated will be emit JSON. Don’t worry, we will discuss it all :D.
Here the Inventory is called as the context module. The phoenix docs explains context as follows.
“The context is an Elixir module that serves as an API boundary for the given resource”. In simpler terms, it is like a place, where we put things which belong together.
For example, OrderManagement deals with orders. An order can consist of multiple order items. If we think, these two should be considered together/offers a functionality together, we put them in the same context, say OrderManagement.
Having a context is beneficial due to several reasons. One reason is that we establish an API boundary.
If some module needs to talk with our module, it should use the API. This keeps our modules self-contained and prevents unnecessary coupling with other modules.
See this link for more info about bounded contexts.
The Product here is our model, with all the fields for data. products will be name of the database table and is normally the plural form of Product. The others price:float stock:integer name:string tax:float are the fields/columns in the model/database table with their types. Lets see what files are created by this task.
In the lib/ms/inventory we see a new product.ex file.
Ecto Schema
Schema is like a structure of the Project in database.
This is similar with the typescript interface we defined earlier. changeset is a function, which tranforms the incoming data to the model structure, before being stored into the database.
We can add validation for data or other functionality inside the changesets. Please read the comments in the above to code to learn more.
Now we will look at the migration file in the priv/repo/migrations/_create_products.exs.
As you can see it has a one to one correspondence with the schema in product.ex file.
The add :price, :float is a way of telling that we need to add a field named price with a type float. The change function in a migration file will be called when we do the mix ecto.migrate.
Since they are ran sequentially based on the name of file with date in it, we should we careful when editing the migration files manually.
Views are formally rendering the schema/model we generated. Since we used mix phx.gen.JSON our generated views produce JSON output.
Phoenix also ships with other tasks, which will generate HTML for us. Lets take a look at the views file generated at ms_web/views/product_view.ex
Routing in Phoenix
Now we will look at the generated controller at ms_web/controllers/product_controller.ex.
Before we look at the controllers, we should look at phoenix routes, which will map the CRUD operations using REST on to the functions defined here.
It can be seen in the lib/ms_web/router.ex file. Lets modify it as follows to support REST API, by adding endpoints and routes.
Listing Routes in Phoenix
Lets now execute mix phx.routes. It will give out the following results
Like mentioned before, all verbs were generated from the resources /products from router.ex file.
If running mix fails for reason, always remember to run it from the directory, where our .mix file is present.
Finally we will examine controller.
Mapping Request to Phoenix Controllers
Lets see how a request gets mapped to the functions in the controller. If we send a request like GET /api/v1/products/ it will hit the MsWeb.ProductController.index function.
This can be see from the output of mix phx.routes. It always provides the conn(The plug with information about request) and params(The parameters with the request).
Any variable which starts with an _ in elixir is considered as a variable, whose value we are not interested in.
Since index function doesn’t need any parameters, we can ignore it. We see that it just call _Inventory.list_products() and renders the result via render function call. This will call the render method in the view file.
Now lets look at the list_products function. The file is too big, so lets discuss just the first function.
We see that list_products just calls Repo.all(Product). This is a function to query the database.
What it means is that get me all the rows with Product schema. So now we know when we hit the index function, we will get all the data about that specific model.
Similarly to get a row using an id, we use Repo.get(), and to delete a value we use Repo.delete().
For updation, we use Repo.update. Here we simply update the changeset with the new data and insert it to the database.
Changesets are a very useful concept, for making database operations cleaner and also offers a standard interface to work with database operations.
At this point, I believe we covered the required basics for working with Phoenix and Ecto. More concepts will be explained as we progress through the tutorial.
Lets go ahead and generate the other tables. All generated files follow a similar structure.
An astute, reader will notice that the Product model, we generated before, didn’t include a brandid.
It was on purpose, first we need to generate a brands table to provide a reference to it. Now somehow we need to integrate this into our already existing _Product* model.
We can use the _ecto.gen.migration task to do it. Lets create a new migration as follows.
Now there will be a file with suffix _add_product_details.exs in the priv/migrations/ folder. Lets add the brand_id and some details to the migration. Edit the file as below.
As you can see the brand_id references the brands table. It is a good time to run migrations for already generated models.
Now we need to modify the Product schema to include the brand_id and details. Lets change it as follows.
You will also notice we didn’t add brand_id to cast. It is because adding brand_id will require a brand_id to exist before we create a product. This will be fixed in later sections.
Now lets generate the other tables.
Now we will add paths for newly generated models as follows to the /api/v1/.
Now we have a bare minimum backend, which will respond to our HTTP requests. As you notice our models have some limitations, which we will address in the coming up chapters.
Generate Vue Typescript Application
Getting started with VueJS and Typescript
Typescript is a superset of Javascript with support for static typing.
Static typing can help us in avoiding a category of bugs and also offer very good support with refactoring of code.
For this project we will use vue-cli 3 to generate our Vue app.
To install vue-cli, issue the following command.
Once we have installed, it is time to create our project.
Creating the Project
A new project can be created using vue create.
The console will ask a few questions and once it is finished we have a bare minimum working VueJS app.
I chose Typescript, vue-router, vuex, dart-sass, babel, pwa, unit-jest and e2e-cypress. Once it is finished start the dev server.
Ok, so now we got vue running, but how does it work ?. Lets take a dive into the basic concepts and working of Vue with Typescript.
Vue starts its execution from the main.ts file inside the src/main.ts file.
Routing with Vue-router
Here we make a new Vue instance with router, store and an app.
Router takes care of the routing to views according to the url.
src/router.ts
Vue Components
As you can see here when the path is /(root) we route to component Home. Components are self-contained, reusable blocks in Vue.
Vue components have a template part, which resides inside <template> tag, where we will write html elements. Another part <style> takes care of the styling and the <script> tag part holds the logic for the component.
The files ending with .vue are single-file components. Here we see Home.vue file is imported. Lets see what is inside it, so that we know what we will get when we visit the /.
We see HelloWorld imported from components folder. And it is passed to @Component Typescript decorator.
The @Component is a decorator, which tells vue which components are used in the current component, ie. inside Home component. Since we only use the HelloWorld, we list it there.
And we use the HelloWorld component inside the <template> tag. It is like embedding the contents of that component, inside our own.
You can consider it as small building blocks which make up our current view.
Lets look at the HelloWorld component itself. I omitted unimportant pieces below, like styling information and links.
Vue Component Props
Here we see a new decorator, the @Prop decorator. It is another decorator for making use of vue props.
Props are messages which we pass to the components. For example here we see that we are using {{msg}} inside our <template>, but we haven’t assigned a value to it anywhere in our component.
So from where, will we get this value ?. The props is the answer. Component which embeds this components provides it to us via props. In this case the Home component gives it to us like below. Notice the msg=…value.
So our {{msg}} gets the value Welcome to Your Vue.js …..
State management in Vue
Store is like a database where you store data from components. The store we will use is Vuex.
Vuex is a reactive store, which means all components which use a specific data will be automatically updated, if that data changes.
src/store.ts
We will discuss the state, mutations and actions in a later part of the tutorial.
If we notice the main.ts file, it passes the App.vue into the render function and mounts it to #app id in the public/index.html file.
This means App.vue is the main/entry file.
Inside the App.vue file we have two router-links. The router-link is a like a a href for router.
router-link modifies the router view based on this link in accordance with the router.ts we discussed earlier. This results in changing of displayed component when the link is clicked.
This can be seen by clicking the About button, which points to /about path, which is mapped to views/About.vue component inside router.ts.
The only step left is to mount the vue to some tag in HTML file, which is done by the $mount. Here it mounts to tag with id #app.
Head over to Official docs for an in depth explanation of Vue. The source code for client is available here.
In next chapter, we will discuss about adding multi language support to Vue.
Add Multi-language support
Setting up multi-language support
The inventory manager we will be building should be able to support multiple languages. We use vue-i18n, which comes with typescript support. It can be added using vue-cli.
Here we choose to enable single page i18n support and provide de as fallback language.
The resulting directory structure is as follows.
i18n adds some new files which injects i18n into Vue app.
The i18n.ts will automatically load i18n based on vue.config.js. The .env can be used to put the environment variable to configure the application. More info can be found here.
The src/main.ts will be as follows.
We also got a new locales folder for storing our translations. Currently we have de.json and en.json corresponding to German and English respectively.
Contents of locales/de.json is as follows.
Now lets move the content of newly created HelloI18n.vue to About.vue, so that we can see it action.
We also take value from locales/de.json in $t(‘message’), as we haven’t provided translations in en and Vue fallbacks to de.
The resulting About.vue is as follows.
Now we should see a Hallo i18n von SFC! in our About page along with Hallo von JSON.
In next chapter, we will write our first Vue component.
Building our Homepage view component
Visual Design of Our App
Before we start coding, lets take a look at a rough sketch of our application. The homepage should look as below.
All the blocks except the search bar are buttons. Clicking on these buttons should take us to a different view/component. For example clicking on Products block should get us to Product list view which might look as below.
So lets get started building a the home screen Home.vue.
Our first Vue component
Our component will be very simple, as it just have a few links which will take us to respective components as shown above. We will use bulma for building our interfaces.
This will install bulma and we also need to use font-awesome for our fonts. We will directly link to the CDN inside our index.html file header as shown below. Other parts are ommitted.
In order to link to pages, we will use router-link. The code is explained in the comments inside the Home.vue file. We will use bulma tiles for aligning our boxes.
The working of tiles is explained here. Fontawesome icons are used inside the boxes.
Now our webpage should look as follows.
Thats all for this chapter. We will add our search bar later, once we have our backend ready. In the next chapter, we will start building our inventory management related components, like adding of products and viewing of product lists.
Adding Vuex to Vue
Building Product Related Components
Now we get to the meat of our application. We need to add Products to our inventory. Lets start with writing AddProduct.vue component.
But before we write an AddProduct component, it would be better to build a ProductView.vue component, which can display a product.
Then we can embed this component into our AddProduct component, providing us with a single component for adding/editing and viewing products.
Two-binding in Vue with v-model
The component is introduces a few new concepts. The first is the v-model. v-model creates a two-way data binding with inputs and component data.
In our case we bind <input id=”productName”/> with currentProduct.name variable. currentProduct is a product object passed as a prop.
Props enable to pass data from parent component to child component.
Here our component ProductView.vue is supposed to be embedded in AddProduct.vue component.
AddProduct.vue component will thus provide us with the value for currentProduct which our ProductView.vue component will simply display and bind to its <input>.
As we can see in below example we binding all product attributes to input fields in our template. v-model also automatically takes care of updating the data based on input fields.
The {{variable}} format puts the output of evaluation of Javascript expression variable into the html. It belongs to the template syntax.
Now lets turn our attention to AddProduct.vue component.
As we can see ProductView is embedded inside this component with :currentProduct props set to this.currentProduct. currentProduct is initialized from productService.ts.
We also notice that the button click is bound to onSubmit() function using the v-on directive. v-on directive can be used to listen to DOM events and respond with running of some Javascript code.
onSubmit is an asynchronous function which calls a webservice. async/await enables to handle callback based events in an intuitive linear fashion.
An async function returns a Promise.
Mozilla docs describes a Promise as an object that represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.
In an async function an await call waits for the promise to be completed and provides the value of promise.
In our case the await call waits till the response from the backend comes back. Currently we ignore the output from server, but in future we will handle those.
State Management with Vuex
Before we move onto see how the request is sent and how the backend handles it, lets assume we got back the response and see how to store the response at our client side.
This is useful because, we don’t have to always request the server to give us new data and show the user our cached data at client side.
Vuex is the official goto solution for state management in Vue. It can act as a central store in an application, with state being automatically updated for all components.
It also provides functions like getters, mutations and actions which provides a uniform interface for working with state.
In typescript we use the vuex-module-decorators for working with Vuex. In order to install use the below command.
Vuex got five main parts.
State - This is the data we want vuex to keep. It acts as a single source of truth. Vuex store everything in a single object which acts a tree storing all the data.
Getters - Functions which can retrieve state from the store and compute properties from retrieved state.
Mutations - Way to change the state of Vuex. They are synchronous. Mutations have a string type and a handler. Handlers performs operations on state. Types are used to identify the mutation.
Actions - Actions commit mutations. They are asynchronous. We commit mutations inside actions, once we have the required data.
Modules - In order to keep state manageable we can break down the state into modules. Each module contains its own State, Getters, Mutations and Actions.
The above picture from Vuex homepage show the basic architecture of Vuex.
As we can see components dispatch Actions which commits a Mutation to State.
Our basic store is defined at src/store/index.ts. This is an empty store. We will dynamically register modules to this store while we are declaring the modules.
Now lets take a look at our Vuex module defined at src/store/modules/products.ts. Please take a look at inline comments for better understanding of code.
In my opinion vuex-module-decorators module take away a lot of pain when working with Vuex :).
Now lets look at how our ProductService.ts that talks with the backend and creates/retrieves a product. ProductService.ts resides in src/services folder.
HTTP Requests with Axios
In order to talk with backend we use the axios library. It can be installed as follows.
Here the createProduct function posts the data to the backend service and returns back an AxiosPromise. It contains the output in JSON form from the backend.
HTTP Request Handling with Phoenix
Lets see how this request will be handled by the Phoenix server. As can be seen in src/store/modules/product.ts we have endpoint set to “http://0.0.0.0:4000/api/v1/” and entity set to “products”.
So our createProduct will send a POST request to
/api/v1/products with the data which is of type Product. Our backend matches this with MsWeb.ProductController :create function.
As discussed before we can see this by running below command.
This function will store our data in database and render the JSON which is returned in response. Notice we changed data to product in render(“show.json”). This is just a personal preference.
Similarly we do the same for customer_view.ex by changing data to customer.
As you can see our response will have an id, price, stock, name and tax information. Looking at our createProducts signature we know the response will be of type AxiosPromise<{ “product”: Product }>.
Now only thing we need to do is to define a Product class with this structure and Typescript will provide us with a Product object from JSON response.
Lets take a look at our defined classes corresponding to Products, Brands etc. These classes are designed based on the structure of JSON received from the backend.
One of the major benefits of Typescript is the enforcing of structure for data. For example, in our case we know the attributes a Product/Brand holds.
This enables us to easily convert the JSON received from a web-service into respective objects. This greatly improves the refactorability and maintainability of our code. The types.ts file is present at src/types folder.
Here is our Product definition for Typescript.
Ecto Schemas
If you are wondering, where this structure comes from, it comes from the database layout of our backend. For example consider the lib/ms/inventory/product.ex file from phoenix project.
Inside the schema for products, the timestamps() macro adds updated_at and inserted_at fields. id is automatically generated by ecto.
Other fields like tax, stock, price etc are directly present in the schema.
Converting Ecto Schemas to Typescript Interfaces
We simply convert the types from Elixir/Ecto to Typescript. ie.
float | integer -> number
map -> object
string -> string
Similarly we can convert the all Ecto schemas to Typescript. There are also other ways to generate Typescript schemas like deriving schema from the JSON response from the server.
For example, this page can generate typescript schemas from JSON.
Here is a full listing of the whole types/types.ts file. We have also defined a factory methods to the make an empty product.
If we run our code now, we will notice that the server rejects our request. This is due to CORS security mechanisms.
Adding CORS to Phoenix Server
In order to allow some service A, running in a server A, to access service B in a server B, the server B should provide permissions to service A.
The browsers check these permissions before it makes a web request. This is to improve the security of webservices. By default Phoenix doesn’t allow CORS.
In our since we are developing our frontend and backend separately, we need allow CORS in service A.
To enable CORS we use cors_plug. To use we add the following to deps function in mix.exs file.
Now get our new dependency using below command.
Since we need to allow CORS for all routes we add CORS plug to lib/ms/endpoint.ex file.
Listing all Products
Notice we reused the ProductView again in here. We will soon replace it with a better one in next section.
Adding Components to Vue-Router
Now the only thing remaining is to add our new components to our router file.
Now we can go to add-product to add new products. It should look as below.
We can also go to products to see all of our products.
Since we’ve come this far, lets implement edit the functionality for products. Implementing EditProduct.vue would be straightforward as can reuse our ProductView.vue.
The EditProduct component should take a product_id and should enable us to fetch and update that product. The product_id should be fetched from the URL using vue-router.
For example if we need to edit the product with product_id 1, we will have to go to te URL http://localhost:8081/products/edit/1.
Below are the contents of src/components/product/EditProduct.vue
Vue enables conditional rendering of a block using v-if directive. In our case the <form> element will only be rendered when the v-if= condition this.currentProduct evaluates to True.
As you can see, we are fetching the product data inside the created() life-cycle hook provided by Vue. created() is called by Vue after our component was instantiated. Vue also provides other hooks like beforeMount and mounted and more.
Refer Vue life cycle hooks for more information. As we can see from productService.ts, updateProduct just send a PUT request to api/v1/products/1, similar to createProduct discussed above.
Now the only step remaining is to see how to retrieve the id(1 here) from a URL like http://localhost:8081/products/edit/1 using vue-router. Just like a component, vue-router also provides props. Consider the new router code snippet below.
Here the we need to make sure the name id in router.ts should match the prop id in EditProduct.vue. Now our EditProduct.vue at /products/edit/1 should look as follows, and changing a value and pressing submit should save the changes to database.
In next chapter we will write a few Vue tests using Jest.
Writing tests with Jest
Testing Our Components
We will be using jest and Vue Test Utils to test our .vue components.
Since we are just starting and since the amount of logic is minimal, it a good time to write some tests.
Since we are using vue-cli all the configurations are already done for us. Vue-cli even provides us with a simple test case for both unit and e2e testing, in our project setup.
In this chapter, we will concentrate on unit tests. So lets get started.
Vue Test Utils is the official Vue unit testing library. Vue Test Utils mounts a Vue component, mocks necessary inputs and returns a Wrapper for the component.
Wrapper as docs put it is an object that contains a mounted component or vnode and methods to test the component or vnode. It provides many useful helper functions for making testing easier.
Lets look at our example.spec.js test in tests/unit directory. If there is a .spec or .test name in filename jest automatically detects it and runs the tests in there.
We can run the unit tests using
Now we got a basic idea on how to write a test case, lets write a simple one for our AddProduct.vue, which tests if our input fields are sent correctly when we click the submit button. The implementation is as given below.
Adding Notifications
Now we will add notifications when Products are created or updated and write a few tests for those.
Here we just added a few translations and added a new showNotification variable, which keeps tracks of whether we should display a notification.
We also turn off the notification after 3 seconds using setTimeout function.
As logic doesn’t require more explanation, lets write a test which checks if notification is displayed when showNotification is true.
Thats all for this chapter. In next chapter, we will start implementing our customer management section.
Build Customer Management Features
In this chapter, we will implement the customer management feature. As you can see the functionality provided by product management is very similar to that should be provided by customer management feature.
The data to be stored is different, but the provided operations are similar like creation, editing etc. For the same reason, we can simply copy the components/product folder and rename it as components/customer.
Rename all occurrences of Product with Customer. So our directory structure looks like below.
Modeling Customer in Typescript
types/types.ts file contains our Customer structure, we generated from the database.
We can provide an implementation for Customer interface just like what we did for Product.
Here we use the Mapped Types in Typescript for cleaner constructor.
Mapped Types allows us to create new types based on old types. For example the Partial<T> we use below can be defined as
We can also define other types like Readonly as below.
Implementing Customer
Our implementation of Customer can be as follows.
Talking with Phoenix Backend
In order to communicate with our Phoenix backend, we need to create a customerService like productService.
For this we copy services/productService.ts as customerService.ts and simply replace product with customer and Product with Customer.
This is possible as we have similar functionality and uniform interface to our backend server. Our resulting customerService is as follows.
In order to persist the customer data, we need to define the Vuex module. Just like customerService it is very similar to store/modules/products.ts.
So we follow the same procedure as above. Copy the products.ts as customers.ts and replace all occurrences of product with customer and Product with Customer and we are ready to go.
Building the Customer View
In order to display the customer details, we need the CustomerView.vue. We can see it is similar to our Productview.vue. The implementation is as follows.
Now implementing CustomerList.vue and AddCustomer.vue is very straightforward as both of them simply use CustomerView.vue component.
To implement CustomerList simply replace Product with Customer and product with customer. Same is the case for AddCustomer component too.
Now the only thing left is to add the customer management functionality to our src/router.ts, so that we can actually use it. In router we add the following code and we are ready for action.
Now go to localhost:8080/customers/add and we can add customers, just like products.
Testing Customer Functionity using Jest
Now for the test cases, just copy test/unit/product and rename to test/unit/customer. Then replace all occurrences of product with customer and Product and Customer.
The resulting test/unit/customer/AddCustomer.spec.ts will be as follows.
As you can see by now, we got a lot of duplicate code. These can be easily fixed by refactoring which we will do in the next chapter.
Refactoring Our Frontend
In last chapter, we added customer management functionality. As you’ve already noticed, we just copied and pasted most of the code for customer management from product management functionality.
In this chapter, we will use Typescript generics to reduce the code duplication. We also will introduce a uniform interface for different models.
Introducing a Base Service
Let’s start with introducing a base class for src/services. We will move all generic functions to baseService.ts.
As you have notice rather than having a function called updateProduct or updateCustomer, in our baseService, we have a function named update. Also the return type is now always AxiosPromise<{‘data’ :T}>,
rather than AxiosPromise<{‘product’ :Product}> or AxiosPromise<{‘customer’ :Customer}>. This provides a uniform interface for all models.
Refactoring Services to use Base Service
Now let’s rewrite our productService and customerService extending baseService.
As you have noticed we have significantly removed duplicate code. Now simply replace all createCustomer or updateProduct function calls with just create or update function calls. This will change a few files.
Since the changes are rather trivial, you can look it up in the bitbucket repo. There are also a few changes in the structure of data returned by our service.
Previously our responses had the structure response.data.product or response.data.customer. Since we changed the return type in baseService, to AxiosPromise<{‘data’: T}>, now all responses have the same structure response.data.data.
Some of you will notice, that we previously changed our backend server to provide product or customer rather than data, due to personal preferences :D. Now we change it back :).
Now our ms_web/views/product_view.ex will be as follows
Similarly our ms_web/views/customer_view.ex will become
Now running our tests, all tests should pass. In next chapter, we will refactor our Phoenix codebase and fix tests.
Refactoring and adding tests for Backend
In this chapter, we will refactor our Phoenix codebase and fix tests. So our step would be to adopt a consistent naming
for database tables and column names. Currently we have camel case and snake case for our table column names. We will
adopt the snake case style for our column names. The first step to achieve this would be to create a migration and then
rename the required columns in that migration.
This will create a file in the priv/repo/migrations/ folder. Lets make the following changes there.
The change function will be executed when we apply the migration. The changes are self-explanatory. Now we need to
rename all occurrences of unitPrices to unit_price and so on in all files. This is a simple search and replace
operation. This results in changes in schema files inside ms/ directory. For example the ms/order_management/order_item.ex file
becomes
This will also change some tests. But by running tests using,
we can confirm that all tests are passing.
MIX_ENV is an environment telling mix about the running context. For example to drop the test database from ecto, we
can use
If we set MIX_ENV=prod, it will drop the production database.
Now lets fix another inconsistency in our naming conventions. All management modules like customer_management,
order_management end with an _management, except inventory. In order to ensure consistency we will rename
inventory to inventory_management. This is also pretty simple by replacing all occurrences of Inventory
with InventoryManagement. Then rename inventory folder to inventory_management and we are done. Just like in
previous case, it will also change some tests. By running tests again, we can confirm that things are working correctly.
In next chapter, we will add order handling functionality.
Adding Order Management
Before we get started with adding order management, let’s make some server-side changes to enforce foreign key constraints.
This will make the database querying easier with respect to foreign keys and also provides us with helper function when working with ecto.
Renaming Fields in Ecto Schema
To start with, lets write a migration to rename all fields in schemas to end with _id if it refers to a foreign key. This will make all foreign keys explicit and also
will be useful in future, when we are writing ecto queries.
And fill in repo/migrations/rename_fields_for_foreign_keys.exs with following content.
Once we have changed the database tables, the schema files should be changed to reflect it.
Let’s take a look at the changes in lib/ms/customer_management/customer.ex file.
As you can see in comments we added has_many. It is a macro, which does not do anything to database, but allows us to access the corresponding Ms.OrderManagement.Order via the foreign key.
It also allows us to easily prefetch it, without writing any SQL.
Similarly we change lib/ms/delivery_management/delivery.ex as follows
belongs_to adds customer_id foreign key to our schema and also allows fetching of Ms.CustomerManagement.Customer corresponding to the customer_id key.
We do the same for lib/ms/inventory_management schemas too. If you find these confusing look at the excellent tutorial at Elixir School about
ecto and associations.
Lets run our migrations
Now that we are done with migrations, let add support for order items for an order. Lets start with adding some constraints on lib/ms/order_management/order_item.ex.
In order for an order_item to be added to database, it should be part of an order. We can enforce it using foreign_key_constraint.
Let’s consider our code.
Here foreign_key_constraint(:order) means that the :order_id of order_items schema, should already exist in orders table. Otherwise the changeset will be invalid.
This makes sure that, we don’t accidentally insert any invalid data.
Modeling JSON representation for Order
Now that we have implemented our order schema with order_items, one question remains. When we ask for an order, should it automatically also provide all associated order_items ?.
In our case, I would say yes. Almost every time, we fetch our order, we will also need order_items. So why not always preload order_items along with an order. In order to render order_items with order,
we will need to provide a serializer for order_item. For this we change our ms/order_management/order_item.ex as follows.
As per the comment @derive uses the Jason encoder to automatically provide us with a JSON representation for order_item. Now the only step left is to always preload all order requests with order_items field.
This can be achieved by updating the lib/ms/order_management.ex code as follows.
For both get_order and create_order, we preload order_items using Repo.preload(:order_items).
Let’s run our tests to see if everything went right.
Fixing Failing Tests
Unfortunately some of our tests fail. Most of them fail because, when testing order_items, we don’t provide a valid order_id.
It can be easily fixed, by first creating an order and then passing the order_id from the created order into the order_item request. Now some other tests fails due to mismatch in struct structure in Elixir.
It can fixed by replacing all :atom keys in structures to string. For example, we need to change from %{name: “Name”} to %{“name” ⇒ “Name”}. This is required because in Elixir :name is not the same as “name”.
Tests also expose that we have a bug in handling cascading deletes. Our current migration in repo/migrations/20190613185803_create_order_items.exs works as follows.
To allow deleting of products, we need to change on_delete: :nothing to on_delete: :delete_all. This deletes all order_items when a product is deleted.
Note: In some cases, it is required to preserve all order history. But as you will see later, this can be easily fixed.
So lets create a new migration to fix this issue.
And add the following code
Run our migration and then the tests.
Now all our tests are green again :D.
So now that we have completed the whole backend requirements for implementing orders, we will start with implementing the UI for orders.
Building Order Views
Before we start implementing UI for order management, we need to change the unitPrice and creationDate to unit_price and creation_date in client side too.
This is pretty easy as all our types are defined in src/types/types.ts.
The changed code is as follows.
Now we get started with implementing our order management UI. Our workflow for adding a new order should be as follows.
As a starting step, lets copy required files from product management and rename all occurrences of Product to Order and all product to order. This also applies to files also.
So our component directory structure for order should look as follows
So our src/components/order/OrderView.vue looks as below.
As you can see we only display the order message and order items. There is no information about customers. There is also no UI to choose order items.
In order to have a good user experience, we will have implement something like an autocomplete and drop down to allow to choose products and customers.
If customer doesn’t exist, we should also allow to create a new one. This requires support from backend. For this reason, we will implement this functionality in next chapter. Now we will stick to this placeholder implementation.
Implementing Order Service
Now we will implement the src/services/orderService.ts
So we need to add implementation for getEmptyOrder to our types/types.ts.
Similarly add src/store/modules/orders.ts.
As you can see, this file is almost the same like src/store/modules/products.ts. We can just copy products.ts and replace Product with Order and product with order to get this file.
So now the only thing left to do is to add the new order components to router.ts.
In next chapter, we will add an autocomplete with search functionality and improve the UI for creating new orders.
Adding new features to Order Management
In last chapter, we added a bare minimum order management functionality, where you can add only an order message.
In this chapter, we will add functionality to add order items and a customer to the order.
The order items and customer can be chosen with an autocomplete menu. So let’s get started.
Implementing Autocomplete
The autocomplete functionality for products will be based on product name.
So if the user enters a part of the product name, we should show the product with its full name.
Our current autocomplete will be naive and will find products using a LIKE query in SQL.
We will also cache all results using ETS. This will help to reduce the database lookups.
In order to implement the autocomplete functionality, we need to add a new endpoint to our server. Let’s place it under products/search.
Lets add our product search functionality with InventoryManagement.search_product function inside lib/ms/inventory_management.ex.
Here we use ilike which does a case-insensitive LIKE in SQL.
The ^ inside ilike is used to bind to dynamic values.
In our case we pass our term argument inside SQL query. Now let’s make this functionality available to our web controllers.
We will pass all required data with GET request as url parameters. It is a better option than passing data with the body of the request.
Some firewalls may even strip the body from a GET request.
Inside lib/ms_web/controllers/product_controller.ex, we add the search function to handle our searches.
Now we just need to register an endpoint in lib/ms_web/router.ex.
An automated test case would be a good idea, to test this functionality. This will also be useful when we add the cache.
Inside test/ms_web/controllers/product_controller.exs
Now it’s a good time to add a cache to our product search. We will use a GenServer to do our caching.
Our cache will be run in a separate process and this process will hold our ets tables making our problem more resilient to crashes.
Before we add the cache, we got a few basics to cover. These basics will help you understand why Elixir/Erlang is very suited to writing fault-tolerant applications.
Since I don’t want to repeat what I’ve already written, please read this and come back.
We will add a our caching GenServer inside lib/ms/cache_management/product.ex file.
Now we modify our lib/ms_web/controllers/product_controller.ex to make use of our cache.
Add Cache to Application
Now all that is left to do is the ask our Phoenixapplication to start our cache with it.
To do this add our cache inside the children list inside start in file lib/ms/application.ex. Now our cache is supervised and if it crashes will be
rebooted based on strategy defined in our Application.
All our tests should pass after the addition of cache.
Visualizing GenServer with Observer
For those of you who are wondering if it is possible to visualize our new GenServer, there is a good news.
Run the phoenix app inside iex and we should be able to use the observer to see the whole application.
Notice the Elixir.Product at the bottom, that is our product cache. It was named based on the name we gave in start_link in ms/cache_management/product.ex file.
In a similar fashion, we can implement our Customer Cache. Please take a look at code to see the actual implementation.
Now that our backend is ready, lets move on to the frontend. Our frontend will be pretty crude in the beginning. We will fix those in coming chapters :D.
As you know, our AddOrder is pretty primitive. In order to make it more user friendly, we need to have an autocomplete functionality in UI. Autocomplete should work for both customer and products.
Building Autocomplete UI Vue Component
The contents of src/components/utils/AutoComplete.vue is as follows.
Now that we got a generic autocomplete component which can take values based on a search function, let’s build a view to show an Order Item to the user.
Before we implement view we need to implement OrderItemImpl class. It can be as follows.
Our current backend implementation, doesn’t provide product_id for orderitem. To fix it, we add :product_id to schema present in lib/ms/order_management/order_item.ex file as follows.
Now we can implement OrderItemView which is as follows contained in file src/components/orderitem/OrderItemView.vue.
Our component is bare minimum and doesn’t even have a decent UI. But we will fix it later. A good addon for AddOrder.vue would be a button to adjust the quantity of products ordered.
So we will add a button to increment the quantity and one to decrement it. We will build it into OrderView.vue as an Order Item ultimately belongs to an order.
Integrating Autocomplete with Orders
As you notice, the changes are minor and lets wire up everything with our AddOrder.vue component.
As per our order creation workflow, the user should choose the customer first and then only will have the option to add order items(products). We can model it, by using multiple stages for the order creation.
The first stage will be ChooseCustomer and then proceed to ChooseOrderItems and ending with OrderCreated. We use OrderCreationStage union type in Typescript to represent each state.
Lets take a look at our code at src/components/order/AddOrder.vue.
The completed src/components/order/AddOrder.vue is as follows.
Adding Search to Base Service
Now the only thing left is to add search functionality to src/services/baseService.ts.
Now our order creation page should look as follows.
The src/components/order/EditOrder.vue is very similar to AddOrder.vue, with the only difference that we are unable to change the customer data in EditOrder.vue.
Yes, you are right. It is possible to abstract out the common code in EditOrder.vue and AddOrder.vue and make it smaller. We will leave it as a future task.
Now that we have the UI to handle order edits, lets take a look at backend code for enabling the modification of an order. When an order is edited, we need to pay special attention to order items.
As you know when an order was created, we also created order items inside an order. Due to same reason, when an order changes the order items could also change.
In order to make it easier to implement, when an order is changed, we load all existing order items for the order and delete all of them. Then we create new order items corresponding to the modified order.
The code is for update_order in lib/ms/order_management.ex is as follows.
Now our order editing page should look as follows.
So, now we have implemented order creation and editing functionality for our Order Management System.
Lets see how our tests are doing.
We will see that many of our tests fail. We will see how to fix those failing tests in next chapter.
Fixing failing Elixir tests
In this chapter, we will fix the failing tests. Let’s take a look at a failing test and see how to fix it. Maybe it can also be a bug in our code.
Consider the below error message.
As you can see test complains about missing product_id which is required. This is because our order item requires a reference to a product.
In order to fix this lets add a function to build a product, so that its product id can be passed to the order item. So the product fixture in test/ms/order_management_test.exs is as follows.
Since we use create_product from InventoryManagement, we need to alias alias Ms.InventoryManagement at the top of the file.
And now in to our order_item_fixture function add product_id as follows.
```elixir
def order_item_fixture(attrs \ %{}) do
order = order_fixture()
product = product_fixture()
valid_attrs = Map.merge(
@valid_attrs,
%{“order_id” ⇒ order.id, “product_id” ⇒ product.id}
)
end
```
This should fix 6 out of 7 failures, leaving create_order_item with valid data as the only failing one. As you notice the error is the same like before. Fix is similar to previous fixes. Take a look at code to see exactly how.
Now lets take a look at failing tests in test/ms_web/controllers/order_item_controller_test.exs.
The partial result is as follows.
The error is similar to previous case, where we lack product_id. Here too we add a function to create a product and an order. See the code below.
Also make sure we add alias Ms.InventoryManagement to the top of the file, as we use order_fixture and create_product from Ms.InventoryManagement.
Now we just change the test as follows.
Here the setup [:create_product, :create_order] line says that the result of create_product and create_order is passed as input to the test. In this case it is %{conn: conn, product: product, order: order}.
The conn: conn is automatically injected.
After applying a bit more polish, like renaming attributes to more meaningful names and fixing all compile errors, all our tests will pass. As the code changes are pretty trivial please take a look at the commit.
That’s all folks for this chapter. In next chapter, we will see how to deploy our app to the real world.
Deploying our application using Docker
In this chapter, we will deploy our frontend and backend using docker. During development, we have been using mix to build and run our Phoenix server. When we run our deploy to production, we wont be using mix to run our server. Elixir support releases, which enable to bundle our Phoenix application along with all its dependencies and even the BEAM VM if required. This allows easy deploy to a production machine. There are also many advantages to using release which are explained in detail in official docs
In short, it provides the following benefits.
Code-preloading - This loads all required modules beforehand enabling the system to handle requests as soon as started.
Self-contained - Releases can include BEAM VM, making installation of VM unnecessary on new servers. It also guarantees the correct version of VM.
Configuration - Can be used to tune the system.
From elixir 1.9 onwards releases are built in. Phoenix docs provides a step by step guide to build and deploy phoenix apps. So lets build ourselves a new release.
Initialize a new release
A new release can be initialized as follows.
This will create the rel/ folder at the root. They can be used to tune the system and add configuration. We will get back to this later.
In our config/prod.secret.exs we load the database url and secret key base as environment variables, as shown below.
The issue with the code is that, this code is executed on the compiling machine and copied over to releases.
In simple terms, we get value of DATABASE_URL from the compiling machine and it is not taken from the deployed/production machine. It is a bit strange, but it is true :).
In order to fix this, releases support runtime configuration. Follow the below steps to convert our prod.secret.exs, so that it is loaded at runtime and not at compile time.
Rename config/prod.secret.exs to config/releases.exs
Replace use Mix.Config in config/releases.exs to import Config.
Remove import_config “prod.secret.exs from config/prod.exs file.
The contents of config/releases.exs will be as follows.
As can be seen above, we need to provide values for environment variables SECRET_KEY_BASE and DATABASE_URL.
Phoenix requires SECRET_KEY_BASE to sign and encrypt data, in order to avoid tampering.
We can use the below command to generate the secret and set it as an environment variable in our system.
Database url can be set as follows. It follows the pattern ecto://USER:PASS@HOST/database?port=portNo. Since we ran on port 5433, as opposed to 5433 we need to provide the port too.
Phoenix by default doesn’t start the server with releases. To support this we need to turn on server in config/prod.ex as follows. Just uncomment the below line.
Now we are ready to make our release.
Compiling and build release
Follow the below commands to make release.
Starting the server
Now we will get an error like below.
This is because we didn’t run the mix phx.digest. phx.digest task digests and compresses static files.
Our static Javascript files are in our client repository. So there are two ways to do this.
Either we serve the static files with a server like nginx or we use Phoenix to serve static files too.
If we use Phoenix, we have to build the files in our client repository, copy it over to priv/static and run phx.digest. Here we chose the first option. This decision, makes our elixir releases separate from our UI deployments.
Deploying Static files with Nginx and docker
We want nginx to redirect all /api/v1/ requests to Phoenix and all requests to Vue. Lets take a look at our nginx.conf file. Please take a look at the comments too.
Now lets test-drive this config with an nginx docker container. Before we do this, we need to generate the JS files from client. This can be easily done using npm build.
Now we have a dist folder at root of project with all the js/css files. We can directly mount this folder as the root folder for nginx. In docker it can be done as below.
Here we use –network=”host” for testing purposes. This binds the nginx container to our host, so that all ports in host is accessible to nginx container. This is required as our Phoenix is running on our host machine.
Once we make sure everything works fine, we will everything to a docker-compose file.
As we can see everything works fine and all our apps run in production mode. Now lets write our docker-compose.yml file and Dockerfile which handles everything from building of binaries to deploying them.
Docker file for Phoenix Server
As you know, when you need to build your own container. You need to write a Dockerfile. This file tells docker what commands to execute to build and run our docker container.
Lets take a look at our Dockerfile for backend inside shopmanagementserver folder. Please check the comments inside the file.
Here we use docker multi-stage build.
This allows us to use multiple temporary containers to build our app and copying only required build artifacts from those temporary containers to our app container.
Contents of shopmanagementserver/entrypoint.sh is as follows.
Running Postgres migrations from Docker
You might be wondering why we need this Ms.Release.migrate task in Docker and not before. In previous case we had our postgres database already setup for us by mix ecto.create and mix ecto.migrate.
In production system we don’t have access to mix. So we need to write a migrate script in Phoenix app and call it before starting the phoenix app in production. We place the release.ex file inside lib/ms folder.
The contents are as follows.
This can be triggered in release using eval
Now when our server starts, it will have executed all migrations and hence will be ready to serve requests.
Dockerfile for VueJS client
Similarly we need to build and run our Client. The contents of shopmanagementclient/Dockerfile is as follows.
The only change in shopmanagementclient/nginx/nginx.conf file is as follows. We will host our phoenix app in docker-compose with service name web. That is why we change the name of server from localhost to web. You
will see the docker-compose.yml file soon.
Now the only step left is to combine all build docker containers with docker-compose. The contents of docker-compose.yml file in shopmanagementdeploy is as follows.
Start docker compose with Nginx and Phoenix app
Now our server should be available at localhost.
Let’s try to add a new product. And going to products page, we can confirm everything works fine.
Thats all folks. In next chapter we will make our VueJS client a bit more user friendly.
Making our UI user friendly
In this chapter, we will make our UI user friendly. We have already built interfaces for CRUD operations. We just need to add a nice UI to choose all available functionalities.
Also search functionality would be nice, specifically for Customers and Products. We will also add some links for computing statistics, even though we won’t build the functionality in this part :D .
We will name it HomePage. It will be as follows.
Building a HomePage
So lets get started. We will build a nice page with operations for orders, products and customers.
Since the basic layout of all these pages are the same and requires no advanced JS, we can build a base HTML layout, which can be reused. The contents of src/components/utils/HomePage.vue are as follows.
As you can see we have introduce the HomePageBoxContent type in types/types.ts.
This will ensure that we got all required information to display a bulma tile
Now we will build specific HomePage components. Lets start with components/customer/CustomerHome.vue.
As you can see, there is nothing special with this component. Similarly we can build HomePage for products and orders.
Embedding Views with Vue Slots
Now we lets turn to adding search functionality. If you remember our components/utils/Autocomplete.vue, which offers autocomplete, we maybe able to just reuse it again.
Only requirement for our search would be to display entries in autocomplete specific to product/orders/customers. Our Autocomplete.vue just shows the name all the time, which is not enough.
But it can be easily fixed with vueJS slots. Lets see how we need to modify our Autocomplete.vue.
As you can notice we simply replaced
html
{{item.name}}
with a slot.
html
<slot name="itemView" :item="item">
<!-- Fallback content -->
{{ item.name }}
</slot>
Now we just need to pass values to this slot. Lets look at src/components/order/AddOrder.vue to see how its done. Please take a look at inline comments for details.
Now we just need to build search functionality for products and customers using Autocomplete.vue. Lets take a look at src/components/customer/CustomerSearch.vue.
Similarly, we add components/product/ProductSearch.vue. Now the only task left is to add paths in src/router.ts. Since it is pretty trivial, I leave it out. Please check the code, if you are interested.
So with this we can come to the end of this book. Thanks a lot for sticking with me till the end of this book. I hope you enjoyed working through it as much as I enjoyed writing it.
Have a nice day and take care :)
Leanpub requires cookies in order to provide you the best experience.
Dismiss