P stands for Postgres, H stands for HTMX, and T stands for Typescript. Fastify is hidden but is there. This is a stack for developing fast web applications. You can write your template with JSX to have a composable and type-safe way to write your views.
If you want to develop a server-side first application with Node.js you have a lot of options today. But I feel that the server-first approach in this world of spa vs rsc is a little bit "put aside". This has led to, in my opinion, poor design choices in designing those frameworks. Let's see some of the concerns with the current state of things and how this stack tries to solve them.
We still don't have a well-integrated template engine with Typescript. We have some options like EJS, Pug, and others, but they are not type-safe and the composition apis that they offer is sometimes cumbersome and prone to errors. Fortunately, we have JSX, a well-known and type-safe way to write views. We choose to use @kitajs/html
as a JSX library because it is fast and handles suspense and error boundaries.
If we want to go with something like Astro or Next.js, the HTTP server exposed by both is not going to help you with request validation, serialization, HTTP verb segregation (if you want to have a POST /path and a GET /path you have to use if statements) and other things that are common in a server-side application. We choose Fastify because it is fast, handles serialization and request validation out of the box, and has a good plugin system. The only downside to Fastify is the fact that there is no good starter template for developing server-first web applications. This repository is trying to solve this problem.
I don't like the file routing system that Next.js and other frameworks use. I think that it is a little bit cumbersome to use and it is really hard to do it in a typesafe way. Morehover I don't want to have to name my files following some weird convention. When you want to search for a file that handles the routing part of your product detail page you are going to search for something like product
or detail
, you are not going to search for [...slug].tsx
.
I think that Drizzle in combination with Drizzle-kit is the best ORM currently available for Typescript. I have nothing more to say.
You can clone this repo and run the following
rm -rf .git
git init
pnpm install
pnpm gen-session-key
pnpm dev
and you should have a running server at http://localhost:3000
serving some HTML.
src
contains the source code for the servercomponents
contains the components that are used globallydatabase
contains the database connection and the drizzle tables definitionsplugins
contains the plugins that are used globally (registered automatically)pages
contains the pages that will contain all the business logic of your application. Every file in this directory that ends with.routes.tsx
or.routes.ts
will be registered automatically. Keep in mind that the loader that has the responsibility to load these files is thepages.loader.tsx
file. As you can see there are an error and 404 handlers. Those are registered because the pages are supposed to always return HTML.api
contains the API routes that are exposed by the application. Every file in this directory that ends with.routes.tsx
or.routes.ts
will be registered automatically. Keep in mind that the loader that has the responsibility to load these files is theapi.loader.tsx
so if you want to declare custom handlers logic only for APIs you can do it there. Remember, APIs are supposed to always return JSON.env.ts
validate and expose environment variablesapp.ts
is the file that creates the server (see builder pattern in Fastify documentation)index.ts
is the entry point of the servermain.css
is the main CSS file used by Tailwind CSS
client
contains the client code, and each TypeScript file in this directory will be used as an entry point for the client scripts. The output will be placed in thepublic/dist
directory.public
contains the public files that are served by the servermigrations
contains the database migrations generated by Drizzletsconfig.app.json
is the TypeScript configuration file for the servertsconfig.client.json
is the TypeScript configuration file for the clienttsconfig.json
is the base typescript configuration filetsconfig.test.json
is the TypeScript configuration file for transpiling for the testsdrizzle.cofig.ts
the configuration file for Drizzlecypress.config.ts
the configuration file for Cypresscypress
contains the end-to-end testse2w
contains the end-to-end tests filessupport
contains the support files for the end-to-end tests, like commands and custom assertionsfixtures
contains the fixtures used by the end-to-end tests
This project uses the following technologies:
Make sure to check the documentation of each technology to understand how to use them.
This project uses pnpm
as the package manager. To install the dependencies, run pnpm install
.
To start the development server, run
pnpm dev
To run the unit tests, run
pnpm test
To build for production, run
pnpm build
To run the type check and lint, run
pnpm typecheck
pnpm lint
There is a check command to run various check commands at once. See package.json for more details.
pnpm check
To run the end-to-end tests, run
pnpm e2e
This section varies depending on the deployment platform. There is a Dockerfile that you can use to create a container. The container will serve the application on port 3000. You can build it like this:
docker build -t pht-stack .
After that, you can push that container to a container registry and deploy it to your platform of choice.
This project uses Cypress for end-to-end tests. Locally, while developing your app you should start the dev serve into one terminal and run the Cypress GUI in another terminal. To do that, run:
pnpm dev
pnpm cypress open
This will open the Cypress GUI where you can run the tests. You can also run the tests in headless mode by running:
pnpm e2e
You can easily use Alpine.js in your project. Just install it with
pnpm install alpinejs
Then you can edit like so the client/main.ts
file
import Alpine from 'alpinejs';
import htmx from 'htmx.org';
window.htmx = htmx;
window.Alpine = Alpine;
Alpine.start();
declare global {
interface Window {
htmx: typeof htmx;
Alpine: typeof Alpine;
}
}
// ...rest of the file
To use another database, you need to go into the package.json and remove the current driver dependencies, installing the one that you want to use with Drizzle. Afterward, you need to update the database.ts file with the new way of creating the SQL client (changing the content of the function makeSqlClient
). The last thing you need to do is to update the Drizzle config file with the new driver and connection parameters if needed.
You can configure the client to use Preact or other jsx libraries by changing the jsx
, jsxFactory
, and jsxFragmentFactory
in the tsconfig.client.json
file. Then you need to install the library that you want to use, for example
pnpm install preact
After that, you just create a new entry point file inside the client
directory and you are good to go. You can now in your HTML import the script from public/dist
. Remember to add also an element where the app will be mounted.
If you like to change some default behavior or setup for this starter, feel free to open an issue or a pull request. I will be happy to help you. Make sure that you add some kind of explanation about the changes that you are proposing. So that we can have a meaningful discussion about it.