MVC with Nestjs and Svelte

  • Sascha Aeppli

Let's build a traditional model-view-controller web application with NestJS and Svelte as a template engine.

TL&DR

If you can't wait to get started, this is for you:

git clone git@github.com:munxar/nest-svelte.git
cd nest-svelte
npm i
npm run start:dev

Techstack Overview

What is NestJS?

NestJS is a framework for Node.js backend web applications. It can be compared with Spring Boot or Symfony. Its architecture is heavily influenced by Angular and so it handles TypeScript as a first class citizen.

For me, NestJS was a real game-changer when it comes to Node.js backend development. It comes with a lot of helpful utilities that you usually would add by yourself, but it lets you change almost all aspects of the framework. Additionally, I'm a big fan of di containers, because they make testing and modularity of your app so much easier.

I don't go into more details about NestJS. If you are interested I highly recommend the very good documentation at NestJS Documentation. It covers almost every part of the framework, so I stick to the things we need for our app and give cross-references where needed.

Why, Svelte? Isn't Svelte a "Frontend Framework"?

Well, No. Svelte is a compiler that transforms .svelte components into HTML, JavaScript, and CSS.
But why not use Pug, Handlebars, (insert any express view engine available)?
The answer is components. I personally like using components for building UIs especially in the way Svelte implements them.
As a side note: There is a project Express React Views that lets you render React components with Express. We'll do the same thing, but with Svelte!

Why not Sapper?

Sapper (short for Svelte App Maker) gives us a default template that does server-side rendering (SSR) with Polka or Express. There is a basic router included, that generates routes from your folder/file structure similar to Nuxt.js or Next.js. On top, it includes a basic web service implementation to use it as a progressive web app (PWA).
This is all super helpful but it's very limited when it comes to complex business logic and you'll shortly run into the same problems as with plain Express apps.

Let's Get Started

Create a NestJS application

Enough talk, let's write some code. I start by setting up a basic NestJS application

npx nest new nest-svelte
# I'll choose npm as the package manager
cd nest-svelte
npm run start:dev

The last command spawns a development server. When I enter the URL http://localhost:3000 into my browser, I can see a simple Hello World!.

NestJS Application Structure

# tree -L 2 -I "node_modules|dist"
β”œβ”€β”€ README.md
β”œβ”€β”€ nest-cli.json
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ package.json
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ app.controller.spec.ts
β”‚Β Β  β”œβ”€β”€ app.controller.ts
β”‚Β Β  β”œβ”€β”€ app.module.ts
β”‚Β Β  β”œβ”€β”€ app.service.ts
β”‚Β Β  └── main.ts
β”œβ”€β”€ test
β”‚Β Β  β”œβ”€β”€ app.e2e-spec.ts
β”‚Β Β  └── jest-e2e.json
β”œβ”€β”€ tsconfig.build.json
└── tsconfig.json

It's a fairly simple structure. I drop the details about testing for now and focus on the src/ directory. The entrypoint for every NestJS application is the src/main.ts file.

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Quite simple and self-explanatory. The NestFactory creates an AppModule by resolving all its dependencies and then this instance called app is listening on port 3000.
The app.listen(3000) part looks very similar to express, and in reality that's what's happening under the hoods.
Everything is located in an async function called bootstrap that is called right after it's declaration. This is due to the fact that in Node.js you can't (yet) have top level await calls.
Let's open the file src/app.module.ts next.

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

This looks a lot like Angular to me, and indeed it's quite the same concept:
Everything is composed out of modules, like the one above. The name of the class is not important but helps other developers to get an idea of what its purpose is. The @Module is a class decorator and its argument is a configuration object that contains all the information for NestJS to resolve this module.

  • imports is an array with all dependencies to other modules
  • controllers lists all controllers that this module provides
  • providers are all service classes of this module that should be resolved by the dependency container
  • exports (not used here) lists all service classes that should be visible to other modules that import this module.
    So this is it, with these basic concepts you can already build very complex but yet maintainable, modular, and testable (web) applications.
    Let's check out the controller:
// src/controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

Again a simple class with a decorator @Controller that tells NestJS to treat this class as a web application controller. The method getHello is annotated with @Get() which is the short form of @Get('/'). In other words, NestJS routes get requests with a path of / to the method getHello().
The instance of the controller is provided by NestJSs dependency injection container as a per-request singleton. This dependency container is although responsible for injecting an instance of type AppService into the constructor of the AppController class.
Let's inspect the AppService now:

// src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

And again, a simple class called AppService. The only notable difference is the class decorator @Injectable() for service classes compared to @Controller() on the controller class. Additionally, the service instance is provided as a singleton on the application scope.
Until now I covered the Model and the Controller layer of the MVC architecture, now I'll extend the app with a View layer.

The View Layer

To enable a view engine in NestJS I need to add three things:

  1. add a template engine on express level
  2. add @Render("Home") decorators to the controller method that should render a view.
  3. add a file views/Home.svelte and add some content

Configure The Express Template Engine

First I make changes to the src/main.ts file:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { svelteTemplateEngine } from './svelte-template-engine';
import { Logger } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.engine('svelte', svelteTemplateEngine);
  app.setViewEngine('svelte');

  await app.listen(3000);
  Logger.log(`server listening: ${await app.getUrl()}`)
}
bootstrap();

The first thing I did is importing and using the NestExpressApplication interface as a template argument on the create method. This gives TypeScript more information about the app instance and I can use some express specific methods like app.engine() and app.setViewEngine().
The app.engine() call sets a view engine whenever a .svelte file is requested. The setViewEngine() call on the other hand tells express what the default view extension is, if we omit it in the call to @Render(). Last but not least I created and imported a file with my svelte template engine implementation:

// src/svelte-template-engine.ts
export function svelteTemplateEngine(filePath: string, options: any, next) {
    next(null, 'todo: implement me!')
}

A template engine in express is just that, a function with three arguments. The filePath contains the absolute path to the requested view, options are what the controller method returns plus global view data, and next is the traditional express middleware-like callback.
The only thing missing is the real implementation, but for now, we close the loop by adding the last puzzle piece for actually executing this code in the AppController

// src/app.controller
import { Controller, Get, Render } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  @Render('Home')
  getHello() {
    return { message: 'NestJS ❀ Svelte' };
  }
}

First I removed the service because I don't need it for this example. Second I added a decorator @Render('Home') on the getHello() method. Decorators are called in the order they are applied, in this case, the order is not important. Third I changed the getHello() signature and return an object instead of the string.

The final step is the template itself

<!-- views/Home.svelte -->
<script>
    export let message;
</script>
<h1>{message}</h1>

The template declares a property with export let message; in the script block and because the controller returns an object with a message attribute, I can access the data in my component.
But first I have to finish the implementation of the template engine.

Implement the Svelte Template Engine

It sounds harder than it is. Luckily the Svelte compiler has a very elegant way to render a .svelte file, but first I install Svelte:

npm i svelte

Now that we have the Svelte compiler available, I activate it by importing the registration script svelte/register inside of the template engine file.

// src/svelte-template-engine.ts
import 'svelte/register';

export function svelteTemplateEngine(filePath: string, options: any, next) {
  const Component = require(filePath).default;
  const { html } = Component.render(options);
  next(null, html);
}

The important part happens in require(filePath). Because the filePath is ending in .svelte, the registered svelte integration kicks in and compiles the file in a full-featured Svelte component inside of Node.js. The call to render() passes the options object and renders it with the template.
With the dev server running, I get the output NestJS ❀ Svelte in my browser. Awesome! My Svelte component is rendered.

Improve the Svelte Template Engine

The current implementation doesn't satisfy me. Nor does it generate a valid HTML document, nor can I use features like style-components. Let's fix this.

Layouts

While there are several ways to handle this, I'll go for component composition. So I create a Layout.svelte component like this:

<!-- views/Layout.svelte -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
    <slot />
  </body>
</html>

A basic html page with a element. In Svelte this is where the components content is output if you wrap the component around it.
Back in the views/Home.svelte component I can use this layout.

<!-- views/Home.svelte -->
<script>
  import Layout from './Layout.svelte';
  export let message;  
</script>

<Layout>
  <h1>{message}</h1>    
</Layout>

After restarting the dev server I see a valid html page in my browser. Awesome, but the there is no styling and the page title is missing. I add some style and svelte:head tags to the page.

<!-- views/Home.svelte -->
<script>
  import Layout from './Layout.svelte';
  export let message;  
</script>

<svelte:head>
  <title>Home</title>
</svelte:head>

<Layout>
  <h1>{message}</h1>    
</Layout>

<style>
  h1 {
    color: purple;
  }
  :global(body) {
    background-color: pink;
  }
</style>

This still doesn't work yet. Svelte generates the css and head elements, but I don't use them in the svelte template engine. First I need a way to inject some markup into the generated html, the layout component seams right for this purpose.

<!-- views/Layout.svelte -->
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    %head%
  </head>
  <body>
    <slot />    
  </body>
</html>

Here I use a made up string %head% to have something i can replace. In the template engine it looks like this:

// src/svelte-template-engine.ts
export function svelteTemplateEngine(filePath: string, options: any, next) {
  const Component = require(filePath).default;
  let { html, head, css } = Component.render(options);
  if(css.code) {
    head = `${head}<style>${css.code}</style>`
  }
  next(null, html.replace('%head%', head));
}

The call to Component.render() returns a head and a css attribute. The if-condition checks if there are styles and adds them to the head. After that in the html markup, my string %head% is replaced by it.
Now when I restart the dev server and reload my browser I get all the CSS injected and even the page title is set correctly. Fantastic!

More Pages

Let me finish this little app by adding an about page and a simple nav component.
Add an about component:

<!-- views/About.svelte -->
<script>
  import Layout from './Layout.svelte';
</script>

<svelte:head>
  <title>About</title>
</svelte:head>

<Layout>
  <h1>About</h1>
  <div>
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet non architecto
    magni aut eveniet aperiam possimus debitis praesentium, distinctio magnam ex
    nulla illum unde aliquid vitae excepturi, maiores vel fugiat.
  </div>
</Layout>

Add a method to the controller that handles the /about route:

// src/app.controller.ts
import { Controller, Get, Render } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  @Render('Home')
  getHello() {
    return { message: 'NestJS ❀ Svelte' };
  }

  @Get('/about')
  @Render('About')
  getAbout() {
  }
}
<!-- views/Nav.svelte -->
<nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
</nav>
<!-- views/Layout.svelte -->
<script>
  import Nav from './Nav.svelte';
</script>

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    %head%
  </head>
  <body>
    <header>
      <Nav />
    </header>
    <main>
      <slot />
    </main>
  </body>
</html>

That was quit some code, but you'll get the the idea. From here you can build quite some complex fullstack mvc web applications. So what's the catch?

Things to Improve

One thing that I noticed is that the default tsc compiler doesn't catch .svelte file changes. Every time I make a change in a .svelte component I have to restart the dev server manually.

Because I went with the explicit Layout.svelte component, I have to pass down the state from every page into this component if needed, this could play a little against the DRY principle. To work around this issue, I'd sugest to write global data (current url, logged in user, etc.) to a Svelte store, for example in a express middleware, and then use this store in the components that need to access this data.

Besides that, I can't find a Svelte feature that didn't work, except the obvious cases where you run client-side JavaScript like the event handlers or similar.

Closing Round

I tried to demonstrate how a traditional full-stack application can be built with NestJS and how I integrate Svelte as a template engine to build modern component-based views.
I hope you enjoyed this lengthy ride like I did and learned a few new things.


Tell us what you think