Redesigning 7digital.com

.NET to Node

Dave Aitken / @actionshrimp

7digital

Music API

  • Catalogue
  • Search
  • Downloads / Streaming

7digital.com


Over 20 territories

  • Rewrite from .NET to Node
  • Part of a full redesign
  • How we did it
  • Why??

Before

  • .NET MVC project, pretty good shape, well tested
  • Performance wasn't the greatest - e.g. No async/await to make parallel API calls (old .NET version)
  • CSS wasn't very modular due to the way it had grown
  • Markup a bit messy, no clear class structure
  • Overall pretty pleased with it

Spark views

Markup with arbitrary code, e.g. RenderAction:


<div class="first column width3">
# Html.RenderAction("Index", "Tracks", new { artistId = Model.ReleaseTile.ArtistId, releaseId = Model.ReleaseTile.ProductId, salePrice = Model.ReleaseTile.PriceValue, productUrl = Model.ReleaseTile.ProductUrl, artistUrl = Model.ReleaseTile.DisplayedArtistUrl, displayedArtistName = Model.ReleaseTile.DisplayedArtistName, IsPreOrder = Model.IsPreOrder, CheckPurchaseDetails = Model.IncludesPurchaseDetails });

# Html.RenderAction("Index", "AlsoByRelease", new { id = Model.ReleaseTile.ArtistId, BasedOnReleaseId = Model.ReleaseTile.ProductId });
</div>
                    

Nice and modular, right? Hard to reason about whole pages

Never actually rendered a lot of sub-Actions on their own

Poor performance - spins up a whole MVC context each time

On mobile...

m.7digital.com

  • No editorial content (interviews, etc)
  • Wasn't our project
  • Lack of consistency

Hi-Res Audio

  • Visual refresh
  • Avoid shoehorning

New design

Incremental rollout

Gave it a go

No clear path

  • How to avoid clashes between designs? Particularly for bigger chunks of the page, layout etc. Lots of extra design work
  • Refactoring CSS - painful!

No exciting Hi-Res launch :(

Pretty happy with the backend, keen to ditch views

Markup needed reworking anyway for the re-design!

Mustache

No arbitrary code was appealing after Spark

More testable - everything in view models

Prototype designs with node mustache (we used Grunt for FE build anyway)

Use dummy data to mock up views

Fetching dummy data is a pain, just hook in this handy node 7d api client!

Run on linux, no spinning up Windows VM...

FE guy often WFH on a Mac - Windows environment a bit unfriendly

Both designs in parallel - feature switch

Looked out for X-7D-Responsive-Views header being sent (using browser plugin)

Switch out view engine based on that

(Also checked your IP was based in the 7d office)

Example page


[HasResponsiveView("sign-in")]
public ActionResult Index(string returnUrl)
{
	if (_httpContext.IsResponsive())
	{
		var signInViewModel = BuildResponsiveViewModel(returnUrl);
	}
	else
	{
		var signInViewModel = BuildViewModel(returnUrl);
	}
	return View("SignIn", signInViewModel);
}
                    

(Branching pretty hard)

  • Tools worked better - e.g. SCSS build
  • Bottlenecked at times by Design/CSS, keen to make iterations as quick as possible
  • Noticing it was quicker to wire in data in the prototype
  • Enjoyed environment more, boosted motivation for sometimes tedious work
  • See where this is going...
  • What about shared and "infrastructure" code?
  • Lots needed updating / refactoring

Decided to push ahead and just use node

Putting something live

Validate node

On windows servers...

IISNode

(Used by Azure for hosting node apps)

Oh yeahhh

Also handles multiple node processes per server/LBing them

End of Feb '14

  • Check it works in live
  • Deploy smallest piece that gets heavy traffic
  • Chose AJAX "What's in my basket" endpoint

<handlers>
  <add name="iisnode" path="redesign/server.js"
       verb="*" modules="iisnode" />
</handlers>
                    

<rule name="node-for-basket" stopProcessing="true">
  <match url="^basket/items"/>
  <action type="Rewrite" url="redesign/server.js" />
</rule>
                    

Worked great!

(Apart from the occasional node stack trace showing up in the site header...)

Public beta (April '14)

  • Added a cookie to enable the X-7D-Responsive-Views header

<rule name="set-responsive-views-if-beta-cookie-sent">
  <conditions>
    <add input="{HTTP_COOKIE}" pattern="7digital_beta=" />
  </conditions>
  <serverVariables>
    <set name="HTTP_X_7D_RESPONSIVE_VIEWS" value="true" />
  </serverVariables>
</rule>
<rule name="node-app-if-header" stopProcessing="true">
  <conditions>
    <add input="{HTTP_X_7D_RESPONSIVE_VIEWS}" pattern="^true$" />
  </conditions>
  <action type="Rewrite" url="reboot/server.js" />
</rule>
                    

Is there anything in particular you'd like to see on the new store?

BRING BACK THE FUNK!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                    

Building away

  • Beefing up prototype snippets
  • Using Express (Sinatra-inspired)

Lots of 7D API calls


router.get('/artist/:artistSlug/release/:releaseSlug',
  handleReleasePage);

function handleReleasePage(req, res, next) {

  api.Catalogue().getReleaseIdByUrl({
    artistSlug: req.params.artistSlug,
    releaseSlug: req.params.releaseSlug
  }, function (err, idLookupRes) {

    if (err) { return next(err); }

    api.Releases().getDetails({
      releaseId: idLookupRes.release.id
    }, function (err, release) {

      if (err) { return next(err); }

      var forView = map.fromRelease(res);
      return res.render('release', forView);
    });
}
                    

Middleware


function userDetails(req, res, next) {...}
                    

expressApp.use(shopConfig);
expressApp.use(userDetails);
...
                    
  • People who opt in are a bit more tolerant
  • Easy to get straight back to the old version
  • Worked on getting critical path done first

Done! (Dec '14)

Responsive design (lots of iterations)

Revamped editorial system

Displaying Hi-Res content

No more legacy DB access \o/

Way better logging/monitoring

But wait

IIS Caching

A nightmare to debug deep inside IIS, on production under load only

Upgraded iisnode


<action type="Rewrite" url="server.js?rewrittenFrom={REQUEST_URI}" />
                    

<iisnode nodeProcessCountPerApplication="4" ../>
                    

A few obstacles

Error handling

function myCallback(err, res) { ... }
throw new Error('lol');

Loss of context if error is not thrown by your own code

Domains


function uncaughtErrorMiddleware(req, res, next) {
  var reqDomain = domain.create();
  reqDomain.name = 'request';
  reqDomain.traceId = req.traceId;

  reqDomain.on('error',
    createUncaughtErrorHandler(serverDomain, req, res));

  reqDomain.run(next);
}
                    

Deprecated :(

Zones? Async Listener?

  • Heavily convention based e.g. (err, res) - the language should protect you from basic slip-ups
  • The convention leads to lots of nesting (callback hell)
  • Refactoring is tricky (well duh)

What we like about node

Simplicity / Malleability

Productive if you know what architecture you're aiming for

Great tooling

npm package for everything

One language everywhere - no context switch

Cross-platform (linux dev environment)

<3 Docker


redis:
  image: redis
  ports:
    - "6379:6379"

sentinel:
  image: joshula/redis-sentinel
  links:
    - redis
  ports:
    - "26379:26379"
  command: sentinel monitor session redis 6379 1 --sentinel monitor objectcache redis 6379 1 -- sentinel down-after-milliseconds session 5000 -- sentinel down-after-milliseconds objectcache 5000
                    

(Yeah, yeah, it is coming to windows..)

Looking back

Should have taken more screenshots...

Old project <3 ?

Many more tooling / deployment choices now

Thanks for listening!