Header-53.jpg

INSIGHTS

Simple Tips for Sustainable Front End Code

When developing a new application or especially when upgrading an old one, it’s easy to make decisions that when looked back on carefully were structural mistakes. Building an application in JavaScript can be especially vulnerable since the front end development world is a still a relatively young and fast moving landscape.

In this article I’ll describe a few simple steps you can take to avoid having structural problems with your application later. This can prevent the painful and frustrating decision to “start over”, and allow for a healthier and longer lived application.

Don’t Inline Important Logic

This is a common recommendation, for both front and back end code. The problem is, when developing an application in a framework like Ember or Angular, we often end up referencing code examples, docs, and blogs, and these do not take the time to demonstrate structural best practices.

In this example, we’re making an Angular 2+ guard that makes sure a user has the proper role. First, see the inlined, bad example:

@Injectable()

export class RoleGuard implements CanActivate {

 

  constructor(private router: Router, private authService: AuthService) {}

 

  canActivate(routeSnapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {

    let neededRole = routeSnapshot.data[“role”];

    if( neededRole === undefined ) {

       return true;

    }

    let roles = this.authService.getCurrentUserRoles();

    let role = roles.find( eachRole => eachRole.roleName === neededRole );

    return (role !== undefined);

  }

}

 

export const ROUTES: Routes = [

  {path: 'login', component: LoginComponent},

  {path: 'admin', component: AdminComponent, canActivate: [AuthGuard, RoleGuard],

   data: { role: “admin” } }

];

 

This can cause a lot of problems in the future. What if you decide to move away from Angular and this code no longer belongs in a guard? There is no need to rewrite it, especially when we can revise and improve it over time. So instead, make sure you pull this code out and hopefully into its own class or even module. In our example, we can move this code to the existing AuthService.

@Injectable()

export class RoleGuard implements CanActivate {

 

  constructor(private router: Router, private authService: AuthService) {}

 

  canActivate(routeSnapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {

    let neededRole = routeSnapshot.data[“role”];

    return this.authService.isAuthorizedForRole(neededRole);

  }

}

 

export const ROUTES: Routes = [

  {path: 'login', component: LoginComponent},

  {path: 'admin', component: AdminComponent, canActivate: [AuthGuard, RoleGuard],

   data: { role: “admin” } }

];

 

 

Now, we have proper separation of concerns. Our security functionality is not based on Angular; Angular only utilizes it. We could reuse this code in future revisions that change frameworks; or even re-use it in a second front end application by further making it part of a library.

By pulling code away in this manner, you can sometimes create some indirection; but that should be covered by good naming conventions and documentation. Don’t name your function “hasRole”; go with something much more specific as a matter of long term planning; such as “isAuthorizedForRole”. Once your code is pulled fully away and is in a library for your company or organization to reuse; this keeps it friendly to developers. Now you can test, reuse, and iterate on this security code separately.

Carefully Consider Dependencies

Generally we are taught never to write code if it already exists; reuse is encouraged in our field. It’s great advice, and with modern JavaScript applications it’s very easy. We can generally search Github or Npm for something we need, and quickly use it. Sometimes however, we act too quickly. For each library, helper, tool, or API you bring into your application, you are adding a dependency.

Dependencies generally stay with an application throughout its lifetime. This can become a problem when a developer pulls your application off the shelf and tries to set it up for building. Generally this is an “npm install”. Even a matter of weeks can break a complex tree of dependencies. This leaves the developer frustrated not only with your project, but with the entire JavaScript landscape. You should carefully consider each new dependency.

First, if you are only using a small utility snippet or function, you might actually consider copying it or using it as an inspiration for your own solution. This happens all the time, and while not ideal it can prevent future breakages.

A second option and usually better choice, is to adjust how your dependency entry is listed in the package file. Never list an entry with * (any version). Consider changing the carrot based version to be either fixed or more restricted. In my projects I’ll generally replace the carrot with a tilde, or remove the version modifier all together. For very small libraries as mentioned above, I’ll remove it.

 

“devDependencies” : {

  “awesome-typscript-loader”: “^2.2.4”,

  “other-tiny-library”: “^2.5.1”

}

 

You can see in this example, if other-library and awesome-typescript-loader both have a release that becomes incompatible, then this project becomes frustratingly broken.

 

“devDependencies” : {

  “awesome-typscript-loader”: “~2.2.4”,

  “other-tiny-library”: “2.5.1”

}

 

We adjust these dependencies slightly to keep compatibility. Not all modules authors respect the versioning process, and they tend to break dependency trees. We could have saved ourselves some time as well, by telling Npm that we want to default to the tilde.

npm config set save-prefix ‘~’

 

This isn’t to ignore upgrades! We have multiple options for keeping on top of versions, especially for security releases. First, we can use something like Yarn to create a separate version lock file. This adds a dependency however, and I don’t prefer it. You can look into helper projects like Npm Check Updates (https://www.npmjs.com/package/npm-check-updates); which can be used later to check your dependency tree for available upgrades and to help you perform them. I’d advise making this part of your automated builds. You can review the revealed upgrade list and have a real person perform the upgrade, that way someone is testing the new dependency tree and the changes are deliberate.

Finally, I need to repeat that you should consider each library carefully before you use it. Ask yourself some simple questions before diving in to solve a problem quickly. It may have long term consequences. Try to make sure it passes this simple test:

  • Is it actively maintained?
  • Are there unit tests?
  • Does it solve a complete set of problems?

For example, you would want to choose a full notification library over a simple toast library. This keeps your dependency list smaller and healthier.

I hope that these examples get you started on making more flexible, future proof and healthy applications! These are just a few examples of the choices and actions you can take to protect your code from aging badly and requiring replacement. It’s always better and more rewarding to be adding awesome new features instead of starting from scratch.

Topics: Architecture & Design, Javascript, front end

Written by George Frick

Leave a Comment