updated readme

This commit is contained in:
rykuno 2024-05-27 13:26:23 -05:00
parent 653e2c22cf
commit c63088b04b
5 changed files with 57 additions and 292 deletions

106
README.md
View file

@ -1,16 +1,19 @@
# Sveltekit - Starter BYOT (Bring your own Tech)
# Sveltekit - Taro Stack
## ❗ Important
If you forked this repository before May 27th, you'll want to view commit `653e2c2`. There was an issue with the Hono context
not correctly parsing routes.
## ❔ What
A scalable, testable, extensible, boilerplate for Sveltekit.
Sveltekit is awesome. File-based routing, SSG/SSR, and having the ability to have a backend attatched to your frontend saves incredible amounts of time and effort.
But that backend sometimes isn't enough. There are some projects that require more powerful and feature rich backends. I'm talking Middleware, guards, pipes, interceptors, testing, event-emitters, task scheduling, route versioning, and so on.
But the default backend sometimes isn't enough. There are some projects that require more powerful and feature rich backends. I'm talking Middleware, guards, pipes, interceptors, testing, event-emitters, task scheduling, route versioning, and so on.
People tend to think that Sveltekit/NextJS are a backend with a frontend attached. **This notion is rediculous** and I'm unsure why its circulated so much. The backend for these frameworks are to facilitate the features in which their frontends promote and make them so powerful.
So whats the answer?
We want to maintain simplicity, elegancy, and a solid DX. Why not just use what appears to be silenly infered to do from the docs? Create a catch-all route and attatch a fully featured api onto the node-process sveltekit already runs on.
So what I've done is attatched Hono, a fully fletched backend, to run on the Sveltekit process and forward all API requests to it.
`/api/[...slugs]`
@ -26,61 +29,64 @@ export const POST: RequestHandler = ({ request }) => app.fetch(request);
## Features
- [x] Email
- [x] E2E Typesafety
- [x] Base Test Coverage
- [x] Authentication
- [x] Email/Passkey
- [ ] OAuth (Implementation varies so maybe not included as base)
- [x] Email verification
- [x] Email updating
- [x] Database
- [x] Migrations
- 🟢 Full E2E typesafety
- 🟢 RPC Client for API Requests
- 🟢 Custom Fetch Wrapper
- 🔴 Deployment Template
- 🟠 Authentication
- 🟢 Email/Passkey
- 🔴 OAuth
- 🟢 Email Update/Verifiaction
## Who is this for?
Me. I created this for myself to bootstrap weekend projects. Its a continous work in progress that I'd like to eventually make ready for public use. That being said, if you see something I could improve I welcome feedback/prs.
## Opinionated Library Selection
My selection of libraries are just what I've found success, **stable** and high cohesion with. This repo should just serve as a guide as to whats possible; not what libary is the best.
#### Auth
- **[Lucia](https://lucia-auth.com)**: Hits the right level of abstraction for me. Hand me the tools to build a secure authentication system and let me implement it to suite my needs
#### Database
## Technologies
- [Lucia](https://lucia-auth.com): Hits the right level of abstraction for me. Hand me the tools to build a secure authentication system and let me implement it to suite my needs
- [Drizzle](https://orm.drizzle.team/) - Drizzle advertises itself as an ORM but I think its deceptive. Its a query builder with a migration client. Everytime I've used an ORM, I find myself fighting it for sometimes the simplist of use cases. Drizzle just gives you type-safety while querying SQL in a native fashion. Learn SQL, not ORMs.
#### Backend
- **[Hono](https://hono.dev/)**: Fast, lightweight, and built on web standards; meaning it can run anywhere you're Sveltekit app can. It's essentially a better, newer, and ironically more stable Express.JS. This provides us an extreemely good foundation to cleanly build ontop of without having to teardown first. It has a zod adapter for validating DTO's which can be shared with the frontend too.
#### Frontend
- **[Sveltekit](https://kit.svelte.dev/)**: After trying Vue, React, Next, and pretty much every frotnend framework in the JS ecosystem, its safe to say I vastly prefer Svelte and its priority of building on web standards.
#### Dependency Injection
- **[TSyringe](https://github.com/microsoft/tsyringe)**: Lightweight dependency injection for JS/TS. If you're familiar with TypeDI, this is essentially the same but actively maintained and used by Microsoft.
- [Hono](https://hono.dev/): Fast, lightweight, and built on web standards; meaning it can run anywhere you're Sveltekit app can. It's essentially a better, newer, and ironically more stable Express.JS. This provides us an extreemely good foundation to cleanly build ontop of without having to teardown first. It has a zod adapter for validating DTO's which can be shared with the frontend too.
- [Sveltekit](https://kit.svelte.dev/): After trying Vue, React, Next, and pretty much every frotnend framework in the JS ecosystem, its safe to say I vastly prefer Svelte and its priority of building on web standards.
## Architecture
We have a few popular architectures for structuring traditional backends
There are a few popular architectures for structuring backends. Technical, Onion, VSA, and the list goes on. I almost always choose
to start with Technical and let the project naturally evolve into one of the others.
- Technical
- Clean/Onion
- Domain Driven Design(DDD)/Vertical Slice Architecture(VSA)
### Folder Structure
* **controllers** - Responsible for routing requests
I choose to start with organizing my backends by **Technical** imeplementions and evolve into one of the others as the project increases in complexity. You don't have to strictly stick to any specific architecture, but they do serve as good guidelines. Alternatively, move things around until it feels right.
* **services** - Responsible for handling business logic.
## Abstraction
* **repositories** - Responsible for retrieving and
storing data.
* **infrastructure** - Handles the implementation of external services or backend operations.
* **interfaces** - Common
* **middleware** - Middlware our request router is responsible for handling.
* **providers** - Injectable services
* **dtos** - Data Transfer Objects (DTOs) are used to define the shape of data that is passed.
* **common** - Anything commonly shared throughout the backend
### File Naming
You might notice how each file in the backend is postfixed with its architectural type(e.g. `iam.service.ts`). This allows
us to easily reorganize the folder structure to suite a different architecture.
For example, if you want to group folders by domain(DDD), you simply drag and drop all related files to that folder.
```
└── events/
├── events.controller.ts
├── events.service.ts
└── events.repository.ts
```
Too many boilerplates lock you into technology choices or straight up throw random libraries to advertise they have more features than their competitors. This is a horrific practice. Just look at the feature list of this ["boilerplate for NextJS"](https://github.com/ixartz/Next-js-Boilerplate#getting-started). The instant this boiler it used, your code is already complex, riddled with external services, impossible to test, require signing up with multiple external services, AND YOU HAVENT EVEN STARTED DEVELOPING YOUR PRODUCT.
## Testing
Testing probably isn't first and foremost when creating an app. Thats fine. You probably shouldnt't be spending time writing tests if your app is changing and pivoting.
Testing probably isn't first and foremost when creating an app. Thats fine. You shouldnt't be spending time writing tests if your app is changing and pivoting.
BUT, a good stack should be **testable** when the time to solidify a codebase arrives. I created this stack with that pinciple in mind. I've provided a examples of how to write these tests under `iam.service.test.ts`.
BUT a good stack should be **testable** when the time to solidify a codebase arrives. I created this stack with that pinciple in mind. I've provided a examples of how to write these tests under `authentication.service.test.ts` or `users.service.test.ts`

View file

@ -1,38 +0,0 @@
CREATE TABLE IF NOT EXISTS "sessions" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL
);
CREATE TABLE IF NOT EXISTS "tokens" (
"id" text PRIMARY KEY NOT NULL,
"token" text NOT NULL,
"user_id" text NOT NULL,
"email" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "tokens_token_unique" UNIQUE("token")
);
CREATE TABLE IF NOT EXISTS "users" (
"id" text PRIMARY KEY NOT NULL,
"avatar" text,
"email" "citext" NOT NULL,
"verified" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
DO $$ BEGIN
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
DO $$ BEGIN
ALTER TABLE "tokens" ADD CONSTRAINT "tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -1,190 +0,0 @@
{
"id": "d887b9c2-b8ca-406b-a093-f6827dc6c256",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "6",
"dialect": "postgresql",
"tables": {
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.tokens": {
"name": "tokens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"tokens_user_id_users_id_fk": {
"name": "tokens_user_id_users_id_fk",
"tableFrom": "tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"tokens_token_unique": {
"name": "tokens_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
}
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "citext",
"primaryKey": false,
"notNull": true
},
"verified": {
"name": "verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -1,13 +0,0 @@
{
"version": "6",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1716599292711,
"tag": "0000_abnormal_earthquake",
"breakpoints": false
}
]
}

View file

@ -1,4 +1,4 @@
import cuid2, { createId } from '@paralleldrive/cuid2';
import { createId } from '@paralleldrive/cuid2';
import { boolean, pgTable, text } from 'drizzle-orm/pg-core';
import { citext, timestamps } from '../utils';
import { relations } from 'drizzle-orm';