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 devand you should have a running server at http://localhost:3000 serving some HTML.
srccontains the source code for the servercomponentscontains the components that are used globallydatabasecontains the database connection and the drizzle tables definitionspluginscontains the plugins that are used globally (registered automatically)pagescontains the pages that will contain all the business logic of your application. Every file in this directory that ends with.routes.tsxor.routes.tswill be registered automatically. Keep in mind that the loader that has the responsibility to load these files is thepages.loader.tsxfile. As you can see there are an error and 404 handlers. Those are registered because the pages are supposed to always return HTML.apicontains the API routes that are exposed by the application. Every file in this directory that ends with.routes.tsxor.routes.tswill be registered automatically. Keep in mind that the loader that has the responsibility to load these files is theapi.loader.tsxso 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.tsvalidate and expose environment variablesapp.tsis the file that creates the server (see builder pattern in Fastify documentation)index.tsis the entry point of the servermain.cssis the main CSS file used by Tailwind CSS
clientcontains 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/distdirectory.publiccontains the public files that are served by the servermigrationscontains the database migrations generated by Drizzletsconfig.app.jsonis the TypeScript configuration file for the servertsconfig.client.jsonis the TypeScript configuration file for the clienttsconfig.jsonis the base typescript configuration filetsconfig.test.jsonis the TypeScript configuration file for transpiling for the testsdrizzle.cofig.tsthe configuration file for Drizzlecypress.config.tsthe configuration file for Cypresscypresscontains the end-to-end testse2wcontains the end-to-end tests filessupportcontains the support files for the end-to-end tests, like commands and custom assertionsfixturescontains 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 devTo run the unit tests, run
pnpm testTo build for production, run
pnpm buildTo run the type check and lint, run
pnpm typecheck
pnpm lintThere is a check command to run various check commands at once. See package.json for more details.
pnpm checkTo run the end-to-end tests, run
pnpm e2eThis 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 devpnpm cypress openThis will open the Cypress GUI where you can run the tests. You can also run the tests in headless mode by running:
pnpm e2eYou can easily use Alpine.js in your project. Just install it with
pnpm install alpinejsThen 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 fileTo 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 preactAfter 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.