DEV Community

loading...
Cover image for Full-Stack Development Should Be Easier

Full-Stack Development Should Be Easier

mvila profile image Manuel Vila Updated on ・7 min read

In the beginning, there were only full-stack developers. We implemented everything in the backend with some PHP or Ruby On Rails and then, with a bit of jQuery running in the frontend, we were done.

But times have changed. Modern web apps require rich user interfaces that can no longer be rendered in the backend.

So we switched to a "single-page application" model with a frontend that entirely manages the user interface.

As for the backend, it is simplified because it only has to manage the domain model and the business logic.

The problem is that we now have to connect the frontend and the backend, and this is where things get complicated.

We build a web API (REST, GraphQL, etc.) that significantly increases the size of our code and results in duplicating our domain model.

In practice, it's like building two applications instead of one.

So we multiply the number of developers, and the overall complexity is such that we divide them into frontend and backend developers.

If you are a half-stack developer, you can only do half the work, and you spend a lot of time communicating with the person in charge of the other half.

If you're a full-stack developer, you're a real hero. You can implement a feature from start to finish in a much more efficient and satisfying way.

Dividing frontend and backend developers kills productivity and ruins all the fun.

But let's be honest, being a full-stack developer today is way too difficult.

Ideally, we should all be full-stack like we were in the beginning. But for that to be possible, we need to dramatically simplify the stack.

Simplifying the Stack

For the simplest projects, it is possible to use a "backendless" solution such as Parse, Firebase, or Amplify. But when the business logic goes beyond CRUD operations, it's not that great.

Something called Meteor came out eight years ago (an eternity in software engineering). The main idea was to simplify the communication between the frontend and the backend, and at that time it was quite revolutionary. Unfortunately, the project has not aged well and it is not suited to today's environment anymore.

Recently, two projects made the buzz — RedwoodJS and Blitz.js. Both also aim to simplify the communication between the frontend and the backend, but with a different approach.

RedwoodJS simplifies the implementation of a GraphQL API and brings together React and Prisma in an opinionated framework.

Blitz.js is also a framework using React and Prisma, but it is built upon Next.js and it strives to eliminate the need for a web API.

These projects are heading in the right direction — simplifying the development of full-stack applications — and I hope they will be successful.

But let me introduce my attempt in the field — a project called Liaison that I have been working on for a year and a half.

Liaison

I created Liaison with an obsession — flattening the stack as much as possible.

A typical stack is made of six layers: data access, backend model, API server, API client, frontend model, and user interface.

With Liaison, a stack can be seen as a single logical layer that reunites the frontend and the backend.

Typical stack vs Liaison stack

The problem is that each layer leads to more code scattering, duplication of knowledge, boilerplate, and accidental complexity.

Liaison overcomes this problem by allowing you to assemble an application in a single logical layer.

Cross-Layer Inheritance

With Liaison, it is possible to inherit the frontend from the backend. It works like regular class inheritance but across layers running in separate environments.

Physically, we still have a separate frontend and backend, but logically, we get a single layer that unites the whole application.

When calling a method from a frontend class that inherits from a backend class, the execution takes place where the method is implemented — in the frontend or the backend — and it doesn't matter for the method's consumer. The execution environment is abstracted away.

Let's see how a full-stack "Hello, World!" would look like with Liaison.

To start, here is the backend:

import {Component, attribute, method, expose} from '@liaison/component';
import {ComponentHTTPServer} from '@liaison/component-http-server';

class Greeter extends Component {
  @expose({set: true}) @attribute() name = 'World';

  @expose({call: true}) @method() async hello() {
    return `Hello, ${this.name}!`;
  }
}

const server = new ComponentHTTPServer(Greeter, {port: 3210});

server.start();
Enter fullscreen mode Exit fullscreen mode

And here is the frontend:

import {ComponentHTTPClient} from '@liaison/component-http-client';

const client = new ComponentHTTPClient('http://localhost:3210');

const Greeter = await client.getComponent();

const greeter = new Greeter({name: 'Steve'});

console.log(await greeter.hello());
Enter fullscreen mode Exit fullscreen mode

Note: This article focuses on the architectural aspect of the stack, so don't worry if you don't fully understand the code.

When running the frontend, the hello() method is called, and the fact that it is executed on the backend side can be seen as an implementation detail.

The Greeter class in the frontend behaves like a regular JavaScript class, and it can be extended. For example, we could override the hello() method like this:

class ExtendedGreeter extends Greeter {
  async hello() {
    return (await super.hello()).toUpperCase();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, when the hello() method is called, the execution happens in both the frontend and the backend. But again, where the execution takes place can be seen as an implementation detail.

From the developer's perspective, the frontend and the backend are a single thing, and that makes everything easier.

Data Persistence

Most applications need to store data and here also things could be greatly simplified.

The idea is not new, a database can be abstracted away with an ORM, and this is the approach followed by Liaison.

In a nutshell, the Storable() mixin brings persistency to your data. For example, take the following class:

import {Component} from '@liaison/component';
import {Storable, primaryIdentifier, attribute} from '@liaison/storable';

class Movie extends Storable(Component) {
  @primaryIdentifier() id;
  @attribute() title;
}
Enter fullscreen mode Exit fullscreen mode

To create and save a Movie, you can do this:

const movie = new Movie({title: 'Inception');
await movie.save();
Enter fullscreen mode Exit fullscreen mode

And to retrieve an existing Movie, you can do this:

const movie = await Movie.get({id: 'abc123'});
Enter fullscreen mode Exit fullscreen mode

Again, there is nothing new here, it is similar to any ORM using the active record pattern.

What changes with Liaison is that the ORM is not restricted to the backend. The cross-layer inheritance mechanism makes the ORM available in the frontend as well.

Conceptually, we therefore still have a single logical layer combining the frontend, the backend, and the database.

User Interface

In a typical application, the user interface and the domain model are completely separated. A few years ago there was a good reason to do so because the user interface was essentially made up of imperative code. But now that we have some functional UI libraries (e.g., React with hooks), it is possible to combine the user interface and the domain model.

Liaison allows you to implement your routes and views as methods of your models.

Here's an example of how to define routes:

import {Component} from '@liaison/component';
import {Routable, route} from '@liaison/routable';

class Movie extends Routable(Component) {
  @route('/movies') static List() {
    // Display all the movies...
  }

  @route('/movies/:id') static Item({id}) {
    // Display a specific movie...
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, a route is simply a URL associated with a method of a model.

Here's how to implement views:

import {Component, attribute} from '@liaison/component';
import React from 'react';
import {view} from '@liaison/react-integration';

class Movie extends Component {
  @attribute() title;
  @attribute() year;
  @attribute() country;

  @view() Home() {
    return (
      <div>
        <this.Heading />
        <this.Details />
      </div>
    );
  }

  @view() Heading() {
    return (
      <h3>
        {this.title} ({this.year})
      </h3>
    );
  }

  @view() Details() {
    return <div>Country: {this.country}</div>;
  }
}
Enter fullscreen mode Exit fullscreen mode

A view is simply a method that returns something based on the attributes of a model, and returning user interface elements is not a problem at all.

Since a view is bound to a model, you probably won't need to use a state manager such as Redux or MobX. The @view() decorator makes sure that the view is re-rendered automatically when the value of an attribute changes.

So we've encapsulated the user interface in the domain model, and that's one less layer to worry about.

Conclusion

I strongly believe that flattening the stack is crucial to make full-stack development more approachable.

Liaison allows you to build a full-stack application in two physical layers — the frontend and the backend — which are gathered in a single logical layer.

It's easier to start a project with as few layers as possible, but that doesn't mean you have to build all your projects that way.

For some projects, it may be a good idea to break down an application into more layers. For example, it may be useful to separate the data access from the backend model or separate the user interface from the frontend model.

No worries. The cross-layer inheritance mechanism allows you to multiply the physical layers while keeping a single logical layer.

If object-oriented programming is not your cup of tea, you won't like Liaison. But please don't reject OOP because you think it offers a poor composition model. JavaScript classes can be functionally defined (e.g., mixins), and are therefore extremely composable.

Check out the Liaison documentation, start building something, and let me know what you think.

This article was originally published on Hacker Noon.

Discussion

pic
Editor guide
Collapse
joelbonetr profile image
JoelBonetR

I've some doubts about this ¿architecture framework?

So you are unifying layers instead on separating them, this could be fine and was the only way to work with software for years, also called monolithic architecture. This is good for little projects but when you need high coverage you need to abstract the layers to maintain each instead on touching it all at the same time (scalability). This, at first sight and reading it superficially and fast, seems a monolith using APIs for the data transfer, isn't it?

What does it adds over the table in comparison with services, microservices or monolithic architectures?

thanks

Collapse
mvila profile image
Manuel Vila Author

I think you didn't get the point. Liaison removes the need to build a web API but doesn't enforce any type of architecture. You can break down your application into as many layers as you want and you can run your components in multiple execution environments.

Collapse
joelbonetr profile image
JoelBonetR

Does It removes the need to build a web API by using graphQL so translating the logic into the front?

Thread Thread
mvila profile image
Manuel Vila Author

GraphQL is a web API, so in terms of layers, it doesn't remove anything. You still have to build an API server and an API client.

Thread Thread
joelbonetr profile image
JoelBonetR

Yup so, if not a monolithic architecture and not API driven, how does it transport data? Thats what I was asking for. The benefits on using this are not clear to me as you can phisically or logically separate or join some layers but at the end you'll need to code them on a manner or another anyway. Doesn't it?

Thread Thread
mvila profile image
Manuel Vila Author

Obviously, you need to code your business logic, but the communication between the layers is handled for you by Liaison. To better understand how it works, please have a look at the documentation.

Thread Thread
joelbonetr profile image
JoelBonetR

Thanks! I understand what do you mean with the extra information on the docs, I'll take a try when possible, at this point I'm more used to react, preact, svelte... but it could be fine to test. I've no much time those days but I'll let it pinned :)

Have you build some Apps with that?

Thread Thread
mvila profile image
Manuel Vila Author

Liaison provides a React integration.

Here are some examples of simple apps:

I build real apps for a customer, but they are not publicly available.

Thread Thread
joelbonetr profile image
JoelBonetR

Understandable, thanks :)

Collapse
crtl profile image
crtl

This is an antipattern.

Collapse
mvila profile image
Manuel Vila Author

Could you expand, please?

Collapse
crtl profile image
crtl

TLDR: Your creating your own problems and then develop solutions to solve them.
The "Liaison stack" is actually the default stack for any small company and if you are running a larger application you probably are seperating your services by default at least for the reason because they are running in the cloud and therefore are seperated by nature.
If you have not reached the scale which requires to split your architecture in such a manner you are fine with running a backend server with database and whatnot serving your frontend client which communicates with the backend api (monolithic).
"Cross-Layer Inheritance" is done by extracting shared code into packages (typescript with interfaces is helpful) and install them as dependencies which is one of the reasons why people are so hyped about node in the first place (I can use JavaScript on Frontend and Backend).
You dont want any of your business logic on the client anyway.
Given the examples you gave and the requirement to have a SPA I can just implement it easily with plain PHP, JavaScript and HTML without having expertise knowledge of it precompiling anything etc..

Sometimes developing is just tedious and you have to write stuff bc of reasons.

Thread Thread
mvila profile image
Manuel Vila Author

Thanks for your detailed answer but I'm afraid you didn't get the point of Liaison.

Obviously, if you can implement everything in the backend in PHP, you don't need Liaison.

But if you have a user interface with some rich user interactions, then you need to manage the user interface in the frontend and you need to build a web API to communicate with the backend.

Liaison removes the need to build a web API but doesn't enforce any type of architecture. You can break down your application into as many layers as you want and you can run your components in multiple execution environments.

Collapse
stojakovic99 profile image
Nikola Stojaković

There is a good reason why we're separating our front-end and our back-end - in this way front-end part doesn't make any assumptions about the back-end, which isn't the case with your framework.

I see you centered all your design around components, but how does it work in the practice if I want to add custom logic? Are components actually used as services? What if, for example, I had a component which depends on another component, for example I had Order component which needs User component for fetching some information?

This is a nice experiment but I can't honestly see using it except for some small CRUD prototypes.

Collapse
mvila profile image
Manuel Vila Author

The frontend and the backend are physically separated but from the point of view of the developer, it's like they are a single thing because the frontend can "inherit" some methods from the backend and call them remotely.

I don't get when you say that a frontend shouldn't make any assumption about the backend. The frontend should at least know what is exposed by the backend and how to call it. Conceptually, calling a REST API or a method is no different.

A component is just a class and you are free to compose multiple components in the same way you would compose multiple classes.

Please check out this example for something that goes beyond a small CRUD prototype.

Collapse
rjaros profile image
Robert Jaros

Your idea of full-stack simplification is very similar to my point of view. I'm also working on an object-oriented framework, which has very similar goals and principles, but uses completely different set of tools :-) It's based on the Kotlin language and you can read more about it in my other posts.

Even if we compete in the some area of web development, I wish you good luck with your project :-)

Collapse
mvila profile image
Manuel Vila Author

Thanks, Robert! Good luck with your project too! :-)

Collapse
guledali profile image
guledali

Please people remind what's wrong with using rails 2020?

Collapse
mvila profile image
Manuel Vila Author

Rails is great to build websites but in my opinion, the single-page application model is a better fit to build web apps.

Collapse
smeijer profile image
Stephan Meijer

Can you explain me why meteor isn't suited for today's environment?

Collapse
mvila profile image
Manuel Vila Author

For example, it seems that Meteor does not fit well with a serverless deployment.

Collapse
smeijer profile image
Stephan Meijer

And that makes it directly "not suited for today's environment"? A bit extreme.

Does serverless support websockets at all? I guess long running connections won't be nice for the bill?

Thread Thread
mvila profile image
Manuel Vila Author

AWS API Gateway supports WebSockets that are managed by Lambda functions in a cost-effective way: aws.amazon.com/blogs/compute/annou...

Collapse
c0derr0r profile image
Irkan Hadi

What about blazor? C# in backend and front-end seems like a simpler way of doing things.

Collapse
mvila profile image
Manuel Vila Author

To my knowledge, Blazor does not flatten the stack.

Collapse
hebashakeel profile image
hebaShakeel

This was reallly helpful.
Looking forward to more blogs from you!!