On this tutorial, I’ll exhibit the way to construct a totally functioning CRUD app utilizing Node for the backend and htmx for the frontend. This can exhibit how htmx integrates right into a full-stack software, permitting you to evaluate its effectiveness and determine if it’s a good selection to your future initiatives.
htmx is a contemporary JavaScript library designed to boost internet purposes by enabling partial HTML updates with out the necessity for full web page reloads. It does this by sending HTML over the wire, versus the JSON payload related to conventional frontend frameworks.
What We’ll Be Constructing
We’ll develop a easy contact supervisor able to all CRUD actions: creating, studying, updating, and deleting contacts. By leveraging htmx, the appliance will provide a single-page software (SPA) really feel, enhancing interactivity and consumer expertise.
If customers have JavaScript disabled, the app will work with full-page refreshes, sustaining usability and discoverability. This strategy showcases htmx’s skill to create trendy internet apps whereas preserving them accessible and Search engine optimisation-friendly.
Right here’s what we’ll find yourself with.
The code for this text might be discovered on the accompanying GitHub repo.
Conditions
To observe together with this tutorial, you’ll want Node.js put in in your PC. When you don’t have Node put in, please head to the official Node download page and seize the right binaries to your system. Alternatively, you would possibly wish to install Node using a version manager. This strategy permits you to set up a number of Node variations and swap between them at will.
Other than that, some familiarity with Node, Pug (which we’ll be utilizing because the template engine) and htmx could be useful, however not important. When you’d like a refresher on any of the above, try our tutorials: Build a Simple Beginner App with Node, A Guide to the Pug HTML Template Preprocessor and An Introduction to htmx.
Earlier than we start, run the next instructions:
node -v
npm -v
It is best to see output like this:
v20.11.1
10.4.0
This confirms that Node and npm are put in in your machine and are accessible out of your command line surroundings.
Setting Up the Venture
Let’s begin by scaffolding a brand new Node venture:
mkdir contact-manager
cd contact-manager
npm init -y
This could create a bundle.json
file within the venture root.
Subsequent, let’s set up the dependencies we’re going to want:
npm i categorical method-override pug
Of those packages, Express is the spine of our app. It’s a quick and minimalist internet framework which affords an easy option to deal with requests and responses, and to route URLs to particular handler features. Pug will function our template engine, whereas we’ll use method-override to make use of HTTP verbs like PUT and DELETE in locations the place the shopper doesn’t help them.
Subsequent, create an app.js
file within the root listing:
contact app.js
And add the next content material:
const categorical = require('categorical');
const path = require('path');
const routes = require('./routes/index');
const app = categorical();
app.set('views', path.be part of(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(categorical.static('public'));
app.use("https://www.Pylogix.com/", routes);
const server = app.hear(3000, () => {
console.log(`Categorical is operating on port ${server.tackle().port}`);
});
Right here, we’re organising the construction of our Categorical app. This contains configuring Pug as our view engine for rendering views, defining the listing for our static property, and hooking up our router.
The applying listens on port 3000, with a console log to substantiate that Categorical is operating and able to deal with requests on the desired port. This setup kinds the bottom of our software and is able to be prolonged with additional performance and routes.
Subsequent, let’s create our routes file:
mkdir routes
contact routes/index.js
Open that file and add the next:
const categorical = require('categorical');
const router = categorical.Router();
router.get('/contacts', async (req, res) => {
res.ship('It really works!');
});
Right here, we’re organising a fundamental route inside our newly created routes listing. This route listens for GET requests on the /contacts
endpoint and responds with a easy affirmation message, indicating that every thing is functioning correctly.
Subsequent, replace the “scripts” part of the bundle.json
file with the next:
"scripts": {
"dev": "node --watch app.js"
},
This makes use of the brand new watch mode in Node.js, which can restart our app each time any adjustments are detected.
Lastly, boot every thing up with npm run dev
and head to http://localhost:3000/contacts/ in your browser. It is best to see a message saying “It really works!”.
Thrilling instances!
Now let’s add some contacts to show. As we’re specializing in htmx, we’ll use a hard-coded array for simplicity. This can hold issues streamlined, permitting us to concentrate on htmx’s dynamic options with out the complexity of database integration.
For these involved in including a database afterward, SQLite and Sequelize are nice decisions, providing a file-based system that doesn’t require a separate database server.
With that stated, please add the next to index.js
earlier than the primary route:
const contacts = [
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' },
{ id: 3, name: 'Emily Johnson', email: '[email protected]' },
{ id: 4, name: 'Aarav Patel', email: '[email protected]' },
{ id: 5, name: 'Liu Wei', email: '[email protected]' },
{ id: 6, name: 'Fatima Zahra', email: '[email protected]' },
{ id: 7, name: 'Carlos Hernández', email: '[email protected]' },
{ id: 8, name: 'Olivia Kim', email: '[email protected]' },
{ id: 9, name: 'Kwame Nkrumah', email: '[email protected]' },
{ id: 10, name: 'Chen Yu', email: '[email protected]' },
];
Now we have to create a template for our path to show. Create a views
folder containing an index.pug
file:
mkdir views
contact views/index.pug
And add the next:
doctype html
html
head
meta(charset='UTF-8')
title Contact Supervisor
hyperlink(rel='preconnect', href='https://fonts.googleapis.com')
hyperlink(rel='preconnect', href='https://fonts.gstatic.com', crossorigin)
hyperlink(href='https://fonts.googleapis.com/css2?household=Roboto:wght@300;400&show=swap', rel='stylesheet')
hyperlink(rel='stylesheet', href='/kinds.css')
physique
header
a(href='/contacts')
h1 Contact Supervisor
part#sidebar
ul.contact-list
every contact in contacts
li #{contact.title}
div.actions
a(href='/contacts/new') New Contact
principal#content material
p Choose a contact
script(src='https://unpkg.com/[email protected]')
On this template, we’re laying out the HTML construction for our software. Within the head part, we’re together with the Roboto font from Google Fonts and a stylesheet for customized kinds.
The physique is split right into a header, a sidebar for itemizing contacts, and a principal content material space the place all of our contact info will go. The content material space at present comprises a placeholder. On the finish of the physique we’re additionally together with the newest model of the htmx library from a CDN.
The template expects to obtain an array of contacts (in a contacts
variable), which we iterate over within the sidebar and output every contact title in an unordered checklist utilizing Pug’s interpolation syntax.
Subsequent, let’s create the customized stylesheet:
mkdir public
contact public/kinds.css
I don’t intend to checklist the kinds right here. Please copy them from the CSS file in the accompanying GitHub repo, or be at liberty so as to add a few of your individual. 🙂
Again in index.js
, let’s replace our route to make use of the template:
router.get('/contacts', (req, res) => {
res.render('index', { contacts });
});
Now while you refresh the web page it’s best to see one thing like this.
To date, all we’ve finished is ready up a fundamental Categorical app. Let’s change that and eventually add htmx to the combo. The subsequent step is to make it in order that when a consumer clicks on a contact within the sidebar, that contact’s info is displayed in the principle content material space — naturally with out a full web page reload.
To start out with, let’s transfer the sidebar into its personal template:
contact views/sidebar.pug
Add the next to this new file:
ul.contact-list
every contact in contacts
li
a(
href=`/contacts/${contact.id}`,
hx-get=`/contacts/${contact.id}`,
hx-target='#content material',
hx-push-url='true'
)= contact.title
div.actions
a(href='/contacts/new') New Contact
Right here now we have made every contact a hyperlink pointing to /contacts/${contact.id}
and added three htmx attributes:
hx-get
. When the consumer clicks a hyperlink, htmx will intercept the press and make a GET request through Ajax to the/contacts/${contact.id}
endpoint.hx-target
. When the request completes, the response might be inserted into the div with an ID ofcontent material
. We haven’t specified any type of swap strategy right here, so the contents of the div might be changed with no matter is returned from the Ajax request. That is the default conduct.hx-push-url
. This can make sure that the worth laid out inhtx-get
is pushed onto the browser’s historical past stack, altering the URL.
Replace index.pug
to make use of our template:
part#sidebar
embrace sidebar.pug
Keep in mind: Pug is white area delicate, so you should definitely use the right indentation.
Now let’s create a brand new endpoint in index.js
to return the HTML response that htmx is anticipating:
router.get('/contacts/:id', (req, res) => {
const { id } = req.params;
const contact = contacts.discover((c) => c.id === Quantity(id));
res.ship(`
<h2>${contact.title}</h2>
<p><robust>Identify:</robust> ${contact.title}</p>
<p><robust>E mail:</robust> ${contact.e-mail}</p>
`);
});
When you save this and refresh your browser, it’s best to now be capable to view the small print of every contact.
HTML over the wire
Let’s take a second to know what’s occurring right here. As talked about at first of the article, htmx delivers HTML over the wire, versus the JSON payload related to conventional frontend frameworks.
We will see this if we open our browser’s developer instruments, swap to the Community tab and click on on one of many contacts. Upon receiving a request from the frontend, our Categorical app generates the HTML wanted to show that contact and sends it to the browser, the place htmx swaps it into the right place within the UI.
So issues are going fairly nicely, huh? Due to htmx, we simply made our web page dynamic by specifying a few attributes on an anchor tag. Sadly, there’s an issue…
When you show a contact, then refresh the web page, our beautiful UI is gone and all you see is the naked contact particulars. The identical will occur if you happen to load the URL instantly in your browser.
The explanation for that is apparent if you consider it. While you entry a URL similar to http://localhost:3000/contacts/1, the Categorical route for '/contacts/:id'
kicks in and returns the HTML for the contact, as we’ve instructed it to do. It is aware of nothing about the remainder of our UI.
To fight this, we have to make a few adjustments. On the server, we have to examine for an HX-Request header, which signifies that the request got here from htmx. If this header exists, then we are able to ship our partial. In any other case, we have to ship the total web page.
Change the route handler like so:
router.get('/contacts/:id', (req, res) => {
const { id } = req.params;
const contact = contacts.discover((c) => c.id === Quantity(id));
if (req.headers['hx-request']) {
res.ship(`
<h2>${contact.title}</h2>
<p><robust>Identify:</robust> ${contact.title}</p>
<p><robust>E mail:</robust> ${contact.e-mail}</p>
`);
} else {
res.render('index', { contacts });
}
});
Now, while you reload the web page, the UI doesn’t disappear. It does, nevertheless, revert from whichever contact you have been viewing to the message “Choose a contact”, which isn’t preferrred.
To repair this, we are able to introduce a case
assertion to our index.pug
template:
principal#content material
case motion
when 'present'
h2 #{contact.title}
p #[strong Name:] #{contact.title}
p #[strong Email:] #{contact.e-mail}
when 'new'
when 'edit'
default
p Choose a contact
And eventually replace the route handler:
if (req.headers['hx-request']) {
} else {
res.render('index', { motion: 'present', contacts, contact });
}
Word that we’re now passing in a contact
variable, which might be used within the occasion of a full web page reload.
And with this, our app ought to stand up to being refreshed or having a contact loaded instantly.
A fast refactor
Though this works, you would possibly discover that now we have some duplicate content material in each our route handler and our principal pug template. This isn’t preferrred, and issues will begin to get unwieldy as quickly as a contact has something greater than a handful of attributes, or we have to use some logic to determine which attributes to show.
To counteract this, let’s transfer contact into its personal template:
contact views/contact.pug
Within the newly created template, add this:
h2 #{contact.title}
p #[strong Name:] #{contact.title}
p #[strong Email:] #{contact.e-mail}
In the principle template (index.pug
):
principal#content material
case motion
when 'present'
embrace contact.pug
And our route handler:
if (req.headers['hx-request']) {
res.render('contact', { contact });
} else {
res.render('index', { motion: 'present', contacts, contact });
}
Issues ought to nonetheless work as earlier than, however now we’ve eliminated the duplicated code.
The subsequent job to show our consideration to is creating a brand new contact. This a part of the tutorial will information you thru organising the shape and backend logic, utilizing htmx to deal with submissions dynamically.
Let’s begin by updating our sidebar template. Change:
div.actions
a(href='/contacts/new') New Contact
… to:
div.actions
a(
href='/contacts/new',
hx-get='/contacts/new',
hx-target='#content material',
hx-push-url='true'
) New Contact
This makes use of the identical htmx attributes as our hyperlinks to show a contact: hx-get
will make a GET request through Ajax to the /contacts/new
endpoint, hx-target
specifies the place to insert the response, and hx-push-url
will make sure that the URL is modified.
Now let’s create a brand new template for the shape:
contact views/type.pug
And add the next code:
h2 New Contact
type(
motion='/contacts',
methodology='POST',
hx-post='/contacts',
hx-target='#sidebar',
hx-on::after-request='if(occasion.element.profitable) this.reset()'
)
label(for='title') Identify:
enter#title(kind='textual content', title='title', required)
label(for='e-mail') E mail:
enter#e-mail(kind='e-mail', title='e-mail', required)
div.actions
button(kind='submit') Submit
Right here, we’re utilizing the hx-post
attribute to inform htmx to intercept the shape submission and make a POST request with the shape information to the /contacts
endpoint. The consequence (an up to date checklist of contacts) might be inserted into the sidebar. We don’t wish to change the URL on this case, because the consumer would possibly wish to enter a number of new contacts. We do, nevertheless, wish to empty the shape after a profitable submission, which is what the hx-on::after-request
does. The hx-on*
attributes mean you can embed scripts inline to answer occasions instantly on a component. You may learn more about it here.
Subsequent, let’s add a route for the shape in index.js
:
...
router.get('/contacts/new', (req, res) => {
if (req.headers['hx-request']) {
res.render('type');
} else {
res.render('index', { motion: 'new', contacts, contact: {} });
}
});
...
Route order is vital right here. When you have the '/contacts/:id'
route first, then Categorical will try to discover a contact with the ID of new
.
Lastly, replace our index.pug
template to make use of the shape:
when 'new'
embrace type.pug
Refresh the web page, and at this level it’s best to be capable to render the brand new contact type by clicking on the New Contact hyperlink within the sidebar.
Now we have to create a path to deal with type submission.
First replace app.js
to offer us entry to the shape’s information inside our route handler.
const categorical = require('categorical');
const path = require('path');
const routes = require('./routes/index');
const app = categorical();
app.set('views', path.be part of(__dirname, 'views'));
app.set('view engine', 'pug');
+ app.use(categorical.urlencoded({ prolonged: true }));
app.use(categorical.static('public'));
app.use("https://www.Pylogix.com/", routes);
const server = app.hear(3000, () => {
console.log(`Categorical is operating on port ${server.tackle().port}`);
});
Beforehand, we’d have used the body-parser package, however I not too long ago realized this is no longer necessary.
Then add the next to index.js
:
router.submit('/contacts', (req, res) => {
const newContact = {
id: contacts.size + 1,
title: req.physique.title,
e-mail: req.physique.e-mail,
};
contacts.push(newContact);
if (req.headers['hx-request']) {
res.render('sidebar', { contacts });
} else {
res.render('index', { motion: 'new', contacts, contact: {} });
}
});
Right here, we’re creating a brand new contact with the information we obtained from the shopper and including it to the contacts
array. We’re then re-rendering the sidebar, passing it the up to date checklist of contacts.
Word that, if you happen to’re making any type of software that has customers, it’s as much as you to validate the information you’re receiving from the shopper. In our instance, I’ve added some fundamental client-side validation, however this could simply be bypassed.
There’s an instance of the way to validate enter on the server utilizing the express-validator package bundle within the Node tutorial I linked to above.
Now, if you happen to refresh your browser and check out including a contact, it ought to work as anticipated: the brand new contact ought to be added to the sidebar and the shape ought to be reset.
Including flash messages
That is nicely and good, however now we’d like a option to inform the consumer {that a} contact has been added. In a typical software, we’d use a flash message — a short lived notification that alerts the consumer concerning the end result of an motion.
The issue we encounter with htmx is that we’re updating the sidebar after efficiently creating a brand new contact, however this isn’t the place we would like our flash message to be displayed. A greater location could be above the brand new contact type.
To get round this, we are able to use the hx-swap-oob
attribute. This lets you specify that some content material in a response ought to be swapped into the DOM someplace aside from the goal, that’s “Out of Band”.
Replace the route handler as follows:
if (req.headers['hx-request']) {
res.render('sidebar', { contacts }, (err, sidebarHtml) => {
const html = `
<principal id="content material" hx-swap-oob="afterbegin">
<p class="flash">Contact was efficiently added!</p>
</principal>
${sidebarHtml}
`;
res.ship(html);
});
} else {
res.render('index', { motion: 'new', contacts, contact: {} });
}
Right here, we’re rendering the sidebar as earlier than, however passing the render
methodology an nameless operate because the third parameter. This operate receives the HTML generated by calling res.render('sidebar', { contacts })
, which we are able to then use to assemble our last response.
By specifying a swap technique of "afterbegin"
, the flash message is inserted on the prime of the container.
Now, after we add a contact, we should always get a pleasant message informing us what occurred.
For updating a contact, we’re going to reuse the shape we created within the earlier part.
Let’s begin by updating our contact.pug
template so as to add the next:
div.actions
a(
href=`/contacts/${contact.id}/edit`,
hx-get=`/contacts/${contact.id}/edit`,
hx-target='#content material',
hx-push-url='true'
) Edit Contact
This can add an Edit Contact button beneath a contacts particulars. As we’ve seen earlier than, when the hyperlink is clicked, hx-get
will make a GET request through Ajax to the /${contact.id}/edit
endpoint, hx-target
will specify the place to insert the response, and hx-push-url
will make sure that the URL is modified.
Now let’s alter our index.pug
template to make use of the shape:
when 'edit'
embrace type.pug
Additionally add a route handler to show the shape:
router.get('/contacts/:id/edit', (req, res) => {
const { id } = req.params;
const contact = contacts.discover((c) => c.id === Quantity(id));
if (req.headers['hx-request']) {
res.render('type', { contact });
} else {
res.render('index', { motion: 'edit', contacts, contact });
}
});
Word that we’re retrieving the contact utilizing the ID from the request, then passing that contact to the shape.
We’ll additionally have to replace our new contact handler to do the identical, however right here passing an empty object:
// GET /contacts/new
router.get('/contacts/new', (req, res) => {
if (req.headers['hx-request']) {
- res.render('type');
+ res.render('type', { contact: {} });
} else {
res.render('index', { motion: 'new', contacts, contact: {} });
}
});
Then we have to replace the shape itself:
- isEditing = () => !(Object.keys(contact).size === 0);
h2=isEditing() ? "Edit Contact" : "New Contact"
type(
motion=isEditing() ? `/replace/${contact.id}?_method=PUT` : '/contacts',
methodology='POST',
hx-post=isEditing() ? false : '/contacts',
hx-put=isEditing() ? `/replace/${contact.id}` : false,
hx-target='#sidebar',
hx-push-url=isEditing() ? `/contacts/${contact.id}` : false
hx-on::after-request='if(occasion.element.profitable) this.reset()',
)
label(for='title') Identify:
enter#title(kind='textual content', title='title', required, worth=contact.title)
label(for='e-mail') E mail:
enter#e-mail(kind='e-mail', title='e-mail', required, worth=contact.e-mail)
div.actions
button(kind='submit') Submit
As we’re passing in both a contact or an empty object to this manner, we now have a straightforward option to decide if we’re in “edit” or “create” mode. We will do that by checking Object.keys(contact).size
. We will additionally extract this examine into a bit helper operate on the prime of the file utilizing Pug’s unbuffered code syntax.
As soon as we all know which mode we discover ourselves in, we are able to conditionally change the web page title, then determine which attributes we add to the shape tag. For the edit type, we have to add a hx-put
attribute and set it to /replace/${contact.id}
. We additionally have to replace the URL as soon as the contact’s particulars have been saved.
To do all of this, we are able to make the most of the truth that, if a conditional returns false
, Pug will omit the attribute from the tag.
That means that this:
type(
motion=isEditing() ? `/replace/${contact.id}?_method=PUT` : '/contacts',
methodology='POST',
hx-post=isEditing() ? false : '/contacts',
hx-put=isEditing() ? `/replace/${contact.id}` : false,
hx-target='#sidebar',
hx-on::after-request='if(occasion.element.profitable) this.reset()',
hx-push-url=isEditing() ? `/contacts/${contact.id}` : false
)
… will compile to the next when isEditing()
returns false
:
<type
motion="/contacts"
methodology="POST"
hx-post="/contacts"
hx-target="#sidebar"
hx-on::after-request="if(occasion.element.profitable) this.reset()"
>
...
</type>
However when isEditing()
returns true
, it would compile to:
<type
motion="/replace/1?_method=PUT"
methodology="POST"
hx-put="/replace/1"
hx-target="#sidebar"
hx-on::after-request="if(occasion.element.profitable) this.reset()"
hx-push-url="/contacts/1"
>
...
</type>
In its replace state, discover that the shape motion is "/replace/1?_method=PUT"
. This question string parameter has been added as a result of we’re utilizing the method-override package, and it’ll make our router reply to a PUT request.
Out of the field, htmx can ship PUT and DELETE requests, however the browser can’t. Which means that, if we wish to take care of a state of affairs the place JavaScript is disabled, we would wish to duplicate our route handler, having it reply to each PUT (htmx) and POST (the browser). Utilizing this middleware will hold our code DRY.
Let’s go forward and add it to app.js
:
const categorical = require('categorical');
const path = require('path');
+ const methodOverride = require('method-override');
const routes = require('./routes/index');
const app = categorical();
app.set('views', path.be part of(__dirname, 'views'));
app.set('view engine', 'pug');
+ app.use(methodOverride('_method'));
app.use(categorical.urlencoded({ prolonged: true }));
app.use(categorical.static('public'));
app.use("https://www.Pylogix.com/", routes);
const server = app.hear(3000, () => {
console.log(`Categorical is operating on port ${server.tackle().port}`);
});
Lastly, let’s replace index.js
with a brand new route handler:
router.put('/replace/:id', (req, res) => {
const { id } = req.params;
const newContact = {
id: Quantity(id),
title: req.physique.title,
e-mail: req.physique.e-mail,
};
const index = contacts.findIndex((c) => c.id === Quantity(id));
if (index !== -1) contacts[index] = newContact;
if (req.headers['hx-request']) {
res.render('sidebar', { contacts }, (err, sidebarHtml) => {
res.render('contact', { contact: contacts[index] }, (err, contactHTML) => {
const html = `
${sidebarHtml}
<principal id="content material" hx-swap-oob="true">
<p class="flash">Contact was efficiently up to date!</p>
${contactHTML}
</principal>
`;
res.ship(html);
});
});
} else {
res.redirect(`/contacts/${index + 1}`);
}
});
Hopefully there’s nothing too mysterious right here by now. At first of the handler we seize the contact ID from the request params. We then discover the contact we want to replace and swap it out with a brand new contact created from the shape information we obtained.
When coping with an htmx request, we first render the sidebar template with our up to date contacts checklist. We then render the contact template with the up to date contact and use the results of each of those calls to assemble our response. As earlier than, we use an “Out of Band” replace to create a flash message informing the consumer that the contact was up to date.
At this level, it’s best to be capable to replace contacts.
The ultimate piece of the puzzle is the flexibility to delete contacts. Let’s add a button to do that to our contact template:
div.actions
type(methodology='POST', motion=`/delete/${contact.id}?_method=DELETE`)
button(
kind='submit',
hx-delete=`/delete/${contact.id}`,
hx-target='#sidebar',
hx-push-url='/contacts'
class='hyperlink'
) Delete Contact
a(
)
Word that it’s good apply to make use of a type and a button to subject the DELETE request. Types are designed for actions that trigger adjustments, like deletions, and this ensures semantic correctness. Moreover, utilizing a hyperlink for a delete motion could possibly be dangerous as a result of engines like google can inadvertently observe hyperlinks, doubtlessly resulting in undesirable deletions.
That being stated, I’ve added some CSS to style the button like a link, as buttons are ugly. When you copied the kinds from the repo earlier than, you have already got this in your code.
And eventually, our route handler in index.js
:
router.delete('/delete/:id', (req, res) => {
const { id } = req.params;
const index = contacts.findIndex((c) => c.id === Quantity(id));
if (index !== -1) contacts.splice(index, 1);
if (req.headers['hx-request']) {
res.render('sidebar', { contacts }, (err, sidebarHtml) => {
const html = `
<principal id="content material" hx-swap-oob="true">
<p class="flash">Contact was efficiently deleted!</p>
</principal>
${sidebarHtml}
`;
res.ship(html);
});
} else {
res.redirect('/contacts');
}
});
As soon as the contact has been eliminated, we’re updating the sidebar and displaying the consumer a flash message.
Taking It Additional
And that’s a wrap.
On this article, we’ve crafted a full-stack CRUD software utilizing Node and Categorical for the backend and htmx for the frontend. Alongside the way in which, I’ve demonstrated how htmx can simplify including dynamic conduct to your internet apps, decreasing the necessity for complicated JavaScript and full-page reloads, and thus making the consumer expertise smoother and extra interactive.
And as an added bonus, the app additionally features nicely with out JavaScript.
But whereas our app is totally purposeful, it’s admittedly a bit bare-bones. When you want to proceed exploring htmx, you would possibly like to take a look at implementing view transitions between app states, or including some additional validation to the shape — for instance, to confirm that the e-mail tackle comes from a selected area.
I’ve examples of each of this stuff (and extra moreover) in my Introduction to htmx.
Other than that, when you’ve got any questions or feedback, please reach out on X.
Completely satisfied coding!