Getting Started with Fastify and Svelte
Fastify is a low-overhead backend web framework that is built on top of the Node.js runtime. Its popularity and attention are rising due to its lightweight design and the plugin ecosystem it comes with. <!--more--> In Fastify, the idea is that everything is a plugin that allows developers to extend its functionalities. This allows us to quickly encapsulate the functionality in our project as a plugin which can then be distributed and used in other projects.
On the client-side, we will use Svelte which is quite different from other modern client-side JavaScript frameworks such as React.
Svelte is a compiler that shifts your work into a compile step rather than using a Virtual DOM. This happens when you build your app by converting the components into highly efficient code.
Goal
In this Fastify and Svelte tutorial, we will build a CRUD app that fetches blog posts.
We will be:
- Setting up a Fastify in a Node.js environment.
- Defining the API routes for our Fastify backend.
- Adding the validation of the request.
- Loading and using Fastify plugins.
- Setting up a Svelte frontend application using
degit
. - Data fetching and building components in Svelte.
Prerequisites
- Basics of programming with the JavaScript and Node.js environment.
- Have Node.js JavaScript runtime installed on your development system.
- An IDE or code editor such as VS Code.
- A basic understanding of REST APIs will be helpful.
- A web browser such as Google Chrome on your development machine.
Setting up the backend with Node.js and Fastify
To start the project, create a folder and name it fastify-svelte-app
. Inside it, we will create another folder for our Svelte client and name it svelte-client
.
Our backend code will be at the root of the main fastify-svelte-app
folder. This is the folder we will start with for now.
Open the terminal and run the command npm init -y
. This initializes npm
with default configurations while creating a package.json
file to manage our dependencies.
Before initializing the Node.js server, we want to add the fastify
and fastify-cors
as a dependency to our project.
npm i fastify fastify-cors --save
Setting up Fastify
We set up the Fastify server inside the index.js
file. First, load the Fastify application object, instantiate it and enable logs to the console.
// Require the framework and instantiate it
const app = require('fastify')({logger: true})
// handle CORS
app.register(require('fastify-cors'), {
origin: true,
methods: ["GET","POST", "DELETE", "PUT", "PATCH"]
})
To consume the API, our server needs a method that we can use to enable Cross-Origin Resource Sharing (CORS) in our server.
CORS is a mechanism that restricts the request of resources on a web server depending on the domain of the HTTP request. When using the CORS middleware, we can allow or limit some routes. In our case, we will allow requests for all the routes.
//Parse Content-Type
app.addContentTypeParser('*', function (request, payload, done) {
var data = ''
payload.on('data', chunk => { data += chunk })
payload.on('end', () => {
done(null, data)
})
})
Next, we declare an index route. Our application will receive and respond with JSON as the parsed data format.
app.get('/', (req, res)=>{
res.send('You are in the index route')
})
app.listen(3000, (err, address)=>{
if(err){
app.log.error(err)
process.exit(1)
}
app.log.info(`Server listening on ${address}`)
})
The app.listen()
method in the above code allows us to listen on port 3000
, so that our application can allow the incoming requests.
Adding controllers
For separation of concerns, we will create a controller
folder in the root folder of the project. Inside the controller
folder, we define the posts.js
file.
In an MVC architecture, a controller is responsible for controlling how the client interacts with an application. It controls the logic flow to determine what response to send back when the server receives a request.
The file will contain some API demo data as an array of objects. Each is a single post with an ID and title field.
We then define all the handlers for the routes in this file. The handler takes request and response as its parameters.
Let's define our post data as an array of objects. The object has the property of ID and title
.
In the posts.js
file, add the following code:
// Demo data
let posts = [
{
id: 1,
title: 'This is an experiment'
},
{
id: 2,
title: 'Fastify and Svelte pretty cool'
},
{
id: 3,
title: 'Another post, here we go!'
}
];
The route handler methods include adding posts, fetching all posts, and deleting posts respectively. When adding posts, we access the body of the request via req.body
.
Lastly, we need to export the handlers as modules to be used in our routes.
// Handlers
const addPost = async (req, res) => {
const id = posts.length + 1 // adding new post will generate a new ID
const newPost = {
id,
title: req.body.title,
}
posts.push(newPost)
return res.send(newPost)
}
const getAllPosts = async (req, res) => {
return posts
}
const deletePost = async (req, res) => {
const id = Number(req.params.id)
posts = posts.filter(blog => blog.id !== id)
return { msg: `Blog with ID ${id} is deleted` }
}
module.exports = {
addPost,
getAllPosts,
deletePost
}
In the next step, we will add the handlers to the Fastify route object.
Creating routes
Using Fastify, we can define route objects as an API endpoint. We then bind all the previously defined handlers to the different routes.
Here is the code to achieve this:
const blogController = require('../controller/blogs')
const routes = [{
method: 'GET',
url: '/api/blogs',
handler: blogController.getAllPosts
},
{
method: 'POST',
url: '/api/blogs',
handler: blogController.addPost
},
{
method: 'DELETE',
url: '/api/blogs/:id',
handler: blogController.deletePost
}
]
module.exports = routes
Now let's test if our server is working or not. Open the terminal again and type:
node server.js
In the console, you should see an output of the server port that our application is running (port 5000
).
Now that we have all routes defined we need to have them registered for in our Fastify application object.
Register Fastify routes
In this step, we’ll register Fastify routes to the app object.
First, we load all the blog routes. Next, we loop over all the routes to register them one by one:
// Register the routes to handle posts
const blogRoutes = require('./routes/blogs')
blogRoutes.forEach((route, index) => {
app.route(route)
})
Now, it’s time to verify if this works.
Start the Fastify server on the terminal using node index.js
. If you visit the browser on http://localhost:3000/blogs/
, should get all the blogs from the demo data in JSON object format.
Our Fastify Node.js backend is complete and now we will head over to Svelte in the next steps.
Adding the frontend with Svelte
Before scaffolding a new Svelte application, create a new Svelte project. We will to use degit
Svelte CLI.
npx degit sveltejs/template svelte-app
To install the dependencies, navigate inside our svelte-app
project and run npm install
:
cd svelte-app
npm install
Open the application folder in your IDE and start the application using Rollup.
npm run dev
Our Svelte folder has an src
folder, which is the folder that will contain all our code.
The two files inside it include:
-
App.svelte
- This is the main Svelte component. It contains tags such asscript
andstyle
-
main.js
- This file is the entry point for the app. It creates a new instance ofApp.svelte
.
import App from './App.svelte'
const app = new App({
target: document.body,
props: {
name: 'world'
}
})
export default app
The attribute target: document.body
indicates that we will render our app inside the HTML body. Initially, it renders the props of the props: { name: 'world' }
.
You will see a rollup.config.js
, which is the configuration file for the rollup packager bundler.
In the rollup.config.js
file, you can see that main.js
is specified as input. main.js
is called by the application, which in turn calls App.svelte
.
If you want to nitpick: main.js
is included within bundle.js
at runtime. bundle.js
gets called by public.html
.
Since this is the boilerplate code, let's start editing and building the component that will connect to our Fastify backend in the next steps.
Create a Blog.svelte Component
To keep things simple, we will use this component to perform all post tasks in our app. Since I'm using bootstrap for styling, head over and add the bootstrap CDN in the public
directory:
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
The tasks of our Svelte component include:
- Fetching posts from the Node.js server within the
<script>
block. - A function to insert new posts which will be held temporarily on the server.
- A function to delete a single post by ID.
Head over to your Posts.svelte
component file and implement the following steps:
Our Posts.svelte
file has <script>
, <section>
container, and <style>
blocks. In between the <script>
tags, we write the JavaScript code to add the dynamic functionality.
To begin, we create a newPostData
object and export it. This object will hold the text of the new todo
with a title
attribute.
The backend determines the ID
by incrementing it each time we add a new post. To insert a new post, we create a function createPost()
that sends a POST
request to our backend in JSON format.
To achieve this, we use the value from the <input>
HTML element and bind it to the newPostData
.
Svelte enriches our HTML markup, so that we can use programming logic such as conditionals and loops. If we click the Add
button, the createPost
method is invoked to create a new post and our backend handles the rest.
<script>
export let newPostData ={
title: "",
}
const createPost = async ()=>{
const new_post = {
title: newPostData.title
}
await fetch('http://localhost:3000/api/blogs', {
method: "POST",
mode: "cors",
headers:{
"Content-Type": "application/json"
},
body: JSON.stringify(new_post)
})
.then(result=>console.log(result))
.catch(err=>console.error(err))
}
</script>
Next, we need to understand how fetchPosts
that retrieves all our posts.
Data fetching is an asynchronous task. Our function and markup use the async-await
syntax to achieve this.
The fetch API makes a network request and returns the posts. On the markup, we check if the fetchPosts
method has retrieved the data before rendering it on the UI. Error handling is implemented on the catch block.
const fetchPosts = (async () => {
const response = await fetch('http://localhost:3000/api/blogs')
return await response.json()
})()
const deletePost = (async ()=>{
})()
Svelte uses a superset of HTML that has:
- A
<section>
tag with a<div>
element as the container. - A form to create new posts
- A label and a text box for entering new tasks.
- An ordered list, which holds a list item for each task and a button to delete the task.
<section>
<div class="container">
<div class="row mt-5">
<div class="col-md-6">
<div class="card p-2 shadow">
<div class="card-body">
<div class="card-title mb-4">
<h2>Add New Post</h2>
<form>
<div class="form-group">
<label for="title">Title:</label>
<input bind:value={newPostData.title}
type="text" class="form-control" id="text" placeholder="Note Post">
</div>
<button on:click|preventDefault={createPost}
type="submit" class="btn btn-primary">Add</button>
</form>
</div>
</div>
</div>
<div class="col-md-">
<div class="card">
<h2 class="card-header">Posts</h2>
<div class="card-body">
{#await fetchPosts}
<p>...waiting</p>
{:then data}
<div>
{#each data as item}
<h3 class="card-title underline">{item.id}</h3>
<p class="card-text">
{item.title}
<span><button class="btn btn-danger">Delete</button></span>
</p>
{/each}
</div>
{:catch error}
<p>An error occurred!</p>
{/await}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
The above Html webpage should look like this:
Deleting a post by ID
To delete a post, we will make a DELETE
request using the fetch API to our backend. The logic is to filter the ID
and return the remaining posts except the filtered one.
Let's create the function that handles this:
const deletePost = async (id)=>{
const response = await fetch(`http://localhost:3000/api/blogs/${id}`, {
method: 'DELETE'
})
const msg = await response.json()
console.log(msg)
// .then(res => console.log(res)).catch(err=>console.log(err))
}
We need to bind this to our delete button in the HTML template.
<span><button class="btn btn-danger" on:click={deletePost}>Delete</button></span>
Thus, we have built a CRUD application to creat, read, update and delete blog posts using Fastify and Svelte.
Conclusion
Fastify and Svelte are modern JavaScript frameworks built for performance and great developer experience. The Fastify framework is rich in the ecosystem of plugins and hence code reusability. Just like Express, Fastify is simple and elegant with a great logging system.
The source code for this tutorial can be found on this GitHub repository.
Happy coding!
Further reading
Peer Review Contributions by: Srishilesh P S