Other articles in the series
The backend related code for this post is available here.
The front-end related code for this post is available at here.
Refactoring Vue with Typescript
In last post, 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 post, 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.
import axios, { AxiosPromise } from 'axios';
export abstract class BaseService<T> {
/*
* endpoint is the actual endpoint like http://localhost:8080/api/v1
* entity is the identifier to find the entity we target. For eg: http://localhost:8080/{endpoint}
* requestPrefix is used when we send data for creation and updating.
* like {${requestPrefix}: data}
*/
private endpoint: string;
private entity: string;
private requestPrefix: string;
constructor(endpoint: string, entity: string, requestPrefix: string) {
this.endpoint = endpoint;
this.entity = entity;
this.requestPrefix = requestPrefix;
}
public getAllRequest(): AxiosPromise<{ 'data': T[] }> {
const response = axios.get(`${this.endpoint}${this.entity}`);
return response;
}
public create(data: T): AxiosPromise<{ 'data': T}> {
const request: {[key:string]: T} = {};
request[this.requestPrefix] = data;
return axios.post(`${this.endpoint}${this.entity}`, request);
}
public update(identifier: number, data: T): AxiosPromise<{ 'data': T}> {
const request: {[key:string]: T} = {};
request[this.requestPrefix] = data;
return axios.put(`${this.endpoint}${this.entity}/${identifier}`, request);
}
public get(identifier: number): AxiosPromise<{ 'data': T}> {
return axios.get(`${this.endpoint}${this.entity}/${identifier}`);
}
public delete(identifier: number): AxiosPromise<any> {
return axios.delete(`${this.endpoint}${this.entity}/${identifier}`);
}
public abstract getEmpty(): T;
}
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.
// productService.ts
import { Product, getEmptyProduct } from '@/types/types';
import { BaseService } from './baseService';
export class ProductService extends BaseService<Product> {
// constructors should call parent class with super()
constructor(endpoint: string, entity: string, requestPrefix: string) {
super(endpoint, entity, requestPrefix);
}
public getEmpty(): Product {
return getEmptyProduct();
}
}
// customerService.ts
import { Customer, getEmptyCustomer } from '@/types/types';
import { BaseService } from './baseService';
export class CustomerService extends BaseService<Customer> {
// constructors should call parent class with super()
constructor(endpoint: string, entity: string, requestPrefix: string) {
super(endpoint, entity, requestPrefix);
}
public getEmpty(): Customer {
return getEmptyCustomer();
}
}
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
defmodule MsWeb.ProductView do
use MsWeb, :view
alias MsWeb.ProductView
def render("index.json", %{products: products}) do
%{data: render_many(products, ProductView, "product.json")}
end
def render("show.json", %{product: product}) do
%{data: render_one(product, ProductView, "product.json")}
end
def render("product.json", %{product: product}) do
%{id: product.id,
price: product.price,
stock: product.stock,
name: product.name,
tax: product.tax}
end
end
Similarly our ms_web/views/customer_view.ex will become
defmodule MsWeb.CustomerView do
use MsWeb, :view
alias MsWeb.CustomerView
def render("index.json", %{customers: customers}) do
%{data: render_many(customers, CustomerView, "customer.json")}
end
def render("show.json", %{customer: customer}) do
%{data: render_one(customer, CustomerView, "customer.json")}
end
def render("customer.json", %{customer: customer}) do
%{id: customer.id,
name: customer.name,
phone: customer.phone,
pincode: customer.pincode,
details: customer.details}
end
end
Now running our tests, all tests should pass. In next section, we will refactor our Phoenix codebase and fix tests.