effect with AI, full stack, it's outrageous π
I recently deployed a new production app, full features, end-to-end, in a couple of weeks:
- Full stack (separate backend and frontend)
- Custom auth
- Database
- Complete UI
- AI integration
Fast and clean leveraging effect and AI. Let me show you some code π
Monorepo required
AI requires the full app context to operate. A monorepo unlocks this (and more).
Nothing fancy really,
pnpmworkspaces and you are good to go π€
It cannot get more simple than that, and AI will be glad:
{
"name": "app",
"private": true,
"scripts": {
"dev": "pnpm --parallel -r dev",
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck"
},
"dependencies": {
"typescript": "^5.9.2"
},
"packageManager": "[email protected]"
}packages:
- "apps/*"
- "packages/*"Shared API signature
Inside packages I have an api folder, with a single main.ts file that exports the full Schema+HttpApi definition, for both backend and frontend.
main.ts contains (in this order):
- Constants
Literalandbrandschemas- Database schemas
- API request and response schemas
- API response error schemas
- Middlewares
- API groups (
HttpApiGroup) - Final
HttpApiexport
Here is an extract:
import {
HttpApi,
HttpApiEndpoint,
HttpApiGroup,
HttpApiMiddleware,
HttpApiSchema,
HttpApiSecurity,
} from "@effect/platform";
import { Context, Schema } from "effect";
export const S_MAX = 10000;
export const S_MIN = 0;
// ...
export const LanguageSelection = Schema.Literal("en", "ja");
const SkillAssignment = Schema.Number.pipe(
Schema.clamp(-1, 1),
Schema.brand("SkillAssignment")
);
// ...
export class User extends Schema.Class<User>("User")({
id: Schema.Number,
username: Schema.String,
createdAt: Schema.DateFromNumber,
}) {}
// ...
export class MessageWithResponse extends Schema.Class<MessageWithResponse>(
"MessageWithResponse"
)({
message: Message,
aiResponse: AiResponse,
}) {}
// ...
export class AuthError extends Schema.TaggedError<AuthError>()(
"AuthError",
{ message: Schema.String },
HttpApiSchema.annotations({ status: 401 })
) {}
// ...
export class AuthMiddleware extends HttpApiMiddleware.Tag<AuthMiddleware>()(
"AuthMiddleware",
{
failure: Schema.Union(AuthError, TokenExpiredError),
security: { authToken: HttpApiSecurity.bearer },
provides: CurrentUser,
}
) {}
// ...
class AuthGroup extends HttpApiGroup.make("auth")
.add(
HttpApiEndpoint.post("login")`/auth/login`
.setPayload(
Schema.Struct({
username: Schema.String,
password: Schema.String,
})
)
.addSuccess(AuthResponse)
.addError(AuthError)
)
.add(
HttpApiEndpoint.post("refreshToken")`/auth/refresh`
.setPayload(Schema.Struct({ token: Schema.String }))
.addSuccess(AuthResponse)
.addError(AuthError)
)
.add(
HttpApiEndpoint.post("logout")`/auth/logout`
.setPayload(Schema.Struct({ token: Schema.String }))
.addSuccess(Schema.Boolean)
.addError(AuthError)
) {}
// ...
export class ServerApi extends HttpApi.make("server-api")
.add(AuthGroup)
.add(AiGroup)
.add(StorageGroup)
.add(SituationGroup) {}This is the foundation for the whole app and AI as well.
Make the API signature as strict and precise as possible, and everything else falls in its place (after various
tsciterations).
The AI will conform to the signature, not stopping until all types are correct.
Backend: Cloudflare Workers
The API is a direct derivation of its signature.
It all starts from the database (I use drizzle-orm+drizzle-kit). This also handles migrations (with wrangler).
The database schema is critical! ποΈ
Design it correctly, and watch the AI "one-shot" all the queries.
import { sql } from "drizzle-orm";
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const userTableName = "user";
export const userTable = sqliteTable(userTableName, {
id: integer("id").primaryKey({ autoIncrement: true }),
username: text("username").notNull().unique(),
passwordHash: text("password_hash").notNull(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch() * 1000)`),
});
//...The entry point of the app is a single fetch that passes the Request to effect, and returns a Response (Cloudflare Worker):
import { HttpApiLive } from "./main";
export default {
async fetch(request, env): Promise<Response> {
const { dispose, handler } = HttpApiLive(env.db);
const response = await handler(request);
await dispose();
return response;
},
} satisfies ExportedHandler<Env>;Everything else is an "implementation detail". At this point, the AI should have enough context to implement anything.
I suggest doing the first few endpoints manually, so the AI has something to copy.
Check out last week's newsletter for more on this.
In fact, I didn't write a single query, it was all done by the AI ππΌββοΈ
Frontend: TanStack Router for max type-safety
The glue between client and server is a single ApiClient client service, derived from the shared API signature:
import { FetchHttpClient, HttpApiClient } from "@effect/platform";
import { ServerApi } from "@app/api"; // π Shared API signature
import { Effect } from "effect";
const API_BASE_URL = "http://localhost:8787";
export class ApiClient extends Effect.Service<ApiClient>()("ApiClient", {
dependencies: [FetchHttpClient.layer],
effect: Effect.gen(function* () {
return yield* HttpApiClient.make(ServerApi, {
baseUrl: API_BASE_URL,
});
}),
}) {}Everything else is about two core libraries:
- TansStack Router: for maximum routing type-safety
tailwindcss: inline styles, easy to ready (and write for the AI)
Also here, start by implementing a couple of routes and styles manually, maybe a few components, and watch the AI infer all the rest πͺ
In summary, these are the "secrets":
- Type safety: if types are strict and enforced, they will guide the AI to the correct implementation
- Full context: monorepo full stack, so that the AI can read everything back to front
- Manual examples: write the core patterns, and let AI just copy those
My coding session went from me typing code, to me (mostly) telling AI what to do, how and where, watch, review and deploy.
Suddenly the new software developer skills became explaining features, architectural know-how, and ability to read and refactor code.
See you next π
