- Folder Structure
- Quick Start
- Global Types
- Middleware
- Helpers
- Error Handling
- Authentication
- Routes and Controllers
- Create a new supabase project.
- Run this script in SQL editor in supabase dashboard. Starter Script
- Clone this repo. And install dependencies with
npm install
- Login to your cloudflare account with
npx wrangler login
- Set secrets with the following names. You can get the supabase keys from your dasboard and run these commands in your terminal.
npx wrangler secret put SUPABASE_URL
npx wrangler secret put SUPABASE_ANON
npx wrangler secret put SUPABASE_SERVICE
-
Now create a new KV namespace for user sessions. And set the KV namespace id in wrangler.toml file. You need to set binding name as
KV_AUTH_SESSIONS
. How ? -
Please dont forget the update supabase types when you change something in your db. Download it and put it in
@/src/types/database.types.ts
file. If you run the exact pgsql script that i provide in second item, there is no need to modify the database file for now. You can download it from supabase dashboard or generate it with supabase CLI. SupabaseDocs -
You are ready to go. Run
npm run start
to start the development server. -
You can deploy your project with
npm run deploy
command. Dont forget to set ENV_MODE to production in wrangler.toml file.
└── src
├── controllers
│ ├── auth
│ │ ├── auth.controller.ts
│ │ ├── auth.types.ts
│ │ └── auth.schema.ts
│ │
│ └── your-controller
│
├── error
│ ├── data
│ │ ├── error-codes.json
│ │ └── errors.ts
│ │
│ ├── CustomError.class.ts
│ └── ErrorHandler.class.ts
│
├── helpers
│ ├── bodyParser.helper.ts
│ └── general.helper.ts
│
├── middlewares
│ ├── auth.mw.ts
│ └── setSupabaseClients.mw.ts
│
├── tests
│
├── types
│ ├── database.types.ts
│ └── ContextEnv.types.ts
│
└── index.ts
In folder controllers there will be an folder for each controller. Each controller folder will contain a controller file, a types file and a schema file.
Schema file is where zod validation schemas are defined. Each controller file will contain a class with the controller name and all the methods that will be used in the routes.
Error folders contains CustomError class, its handler, and error codes which stores in json files. We will create error codes for each error that we will use in the application.
We can define helper funcitons in this folder. For example, we can define a function that will be used in multiple controllers like random string generator.
We can define middlewares in this folder. There is an auth and setSupabaseClients middlewares in the middlewares folder.
We can define tests in this folder. There is no test file in the project yet.
We can define global types in this folder. There is Hono Context Type & Supabase Database types in the types folder.
This folder will contain types that will be used in multiple controller or even in the middlewares. As we mentioned before, the types only used in one controller will be defined in the controller folder.
- There is 2 global types defined in the types folder. ContextEnv and Database types. (You are free to add more types if you need)
Database Types
- This file is generated by Supabase. We can either download it from Supabase or generate it with Supabase CLI. We will use this file when we are creating supabase clients. SupabaseDocs
ContextEnv Types
-
This file defines types for the Hono Context object which known as
c
in controllers. -
We are defining bindings which assing type to
c.env
.
And we are defining variable types which assign return type toc.get('some')
funciton.
// ContextEnv.types.ts
Bindings: {
SUPABASE_URL: string; // from cloudlfare secrets
};
Variables: {
ANON_CLIENT: SupabaseClient;
};
// some.controller.ts
typeof c.env.SUPABASE_URL // string
typeof c.env.SOME_VARIABLE // unknown type
typeof c.get('ANON_CLIENT') // SupabaseClient
typeof c.get('SOME_VARIABLE') // type error you cant get a variable that is not defined in ContextEnv.types.ts
We should be careful when we are defining c
parameter in controllers:
// some.controller.ts
import ENV from '@/types/ContextEnv.types';
const correctMethod = (c:Context<ENV>) : Promise<Response> => {...};
const wrongMethod = (c:Context) : Promise<Response> => {...};
- There is 2 middlewares defined in the middlewares folder. Auth and setSupabaseClients middlewares.
- You can read more about auth middleware in Authentication section.
setSupabaseClients Middleware
- This middlewares creates 2 supabase clients and assign them to
c
object. One of them is SERVICE client which bypass RLS and the other one is ANON clients which is secure. - We can use them in controllers with
c.get('ANON_CLIENT')
andc.get('SERVICE_CLIENT')
// some.controller.ts
const someMethod = (c:Context<ENV>) : Promise<Response> => {
const supabase = c.get('ANON_CLIENT'); // it has type SupabaseClient as we defined in ContextEnv.types.ts
await supabase.from('x').select('*') // we can use supabase client as we normally do
}
- There is 2 helpers defined in the helpers folder. bodyParser and general helper.
- General helper contains some helper functions that will be used in multiple controllers.
bodyParser Helper
-
There is an bodyParser helper in the helpers folder. We will use this helper to parse the request body and also validate the request body with zod schemas. Also it will parse the body neither its json or formData.
-
If validation fails, it will throw an Validation Error so we dont need to check if the validation is successfull or not. If validaiton pass it will return the parsed body with type.
Example usage:
// some.controller.ts
import {schema} from './some.schema.ts';
const someRoute = (c:Context<ENV>) => {
const body = await parseBodyByContentType<typeof schema>(c, schema);
// body is parsed and validated
}
-
When we occur an error, we will throw an error with error code and error message. ErrorHandler will catch the error and send the response with error code and error message.
-
We can throw any type of error with CustomError class instead of returning a response with error message. Like validation errors, database errors, authentication errors etc.
-
In the example below, we will throw an error with error code , data and type. ErrorHandler will catch the error find the message of the error from json files and send the response with error code and error message.
-
The error response has 2 types development and production. And their response are diffrent.
-
For example in dev mode it returns data when the type is ValidationError but not in AuthenticationError.
-
The mod can setted in wrangler.toml ENV_MODE='production' or ENV_MODE='development'
-
Dont forget to set ENV_MODE to production when you are deploying the application.
┌─────┬─────┬─────┬───────┬─────┬─────┐
│ │code │ msg │dev_msg│data │type │
│dev │ + │ + │ + │ + │ + │
│prod │ + │ + │ - │ opt │ - │
└─────┴─────┴─────┴───────┴─────┴─────┘
// some.controller.ts
throw new CustomError('CODE-001',data, ErrorTypes.AuthenticationError);
// some-errors.json
{
"CODE-001": {
"message": "Authentication Error message for user",
"devMessage": "Authentication Error message for dev",
}
}
// response object
{
status:"error",
error: {
"code" : "CODE-001",
"message": "Authentication Error message for user",
"devMessage": "Authentication Error message for dev", // dev mode only
"data": data // always in dev mode, optional in production
"type": "AuthenticationError" // dev mode only
}
}
- It also save this error to the database. We can see the error logs in the supabase admin panel. So you need an error table.
-
Im assuming that you know how supabase authentication works. If you dont know, you can read Supabase Docs
-
We are handling user sessions with custom sessions which stores in KV storage. custom_access_token as Key and Supabase tokens as Value.
-
We passing our custom_access_token to the client with cookie. And we will use it in the middleware to get the user session.
-
You can change the signup redirect url and password-reset redirect url in your wrangler.toml file.
-
Auth middleware check the KV storage if the custom_access_token is exist or not. If it is not exist, it will throw an error.
-
If it is exist, but the supabase tokens expired we are creating new supabase tokens and update the KV storage and then assign it to
ANON_CLIENT
object. if supabase tokens are valid, thats perfect we just assign it toANON_CLIENT
object. -
With this way, we are not creating new supabase tokens for every request. We are creating new supabase tokens only when it is expired. And we can logout the user by deleting the custom_access_token from KV storage. Also we can set custom expiration time for custom_access_token which is limited 1 week for supabase tokens.
-
The auth middleware has role support too. When we are using it in the routes, we can pass the roles as rest parameter. It checks the user data
app_metadata:{role: 'example-role'}
and if the user has the role, it will pass the request to the controller. If not, it will throw an Authorization error. -
We can use auth middleware in the routes like this:
// index.ts
app.get('/some-route', authMw('example-role'), someController.someMethod);
-
In the controllers folder there will be a many folder named by its route. For example we have auth routes under the controllers/auth/... folder. And each folder has 3 files. One for controller itself, one for schema and one for types.
-
The schema file contains zod schemas for validation. The types file contains types only used by controller.
-
Controller file contains the class named by its route. And it has methods that will be used in the index.ts file.
// example.controller.ts
interface IExample {
exampleMethod(c:Context<ENV>) : Promise<Response>;
}
class Example implements IExample {
public static async exampleMethod(c:Context<ENV>) : Promise<Response> {
// some code
}
//...
}
export default new Example();
- And we are using it in the index.ts file like this:
import Example from '@/controllers/example/example.controller.ts';
app.get('/example-method', Example.exampleMethod); // without middleware
app.get('/example-method', authMw(AuthRoles.Any), Example.exampleMethod); // with middleware