Aleksandra Sikora, @aleksandrasays
- open-source engineer @
- 🧗♂️
- org of Wrocław
previously
- .js Maintainer
- cc
🐦 @aleksandrasays
🐙 @beerose
🐘 @aleksandra@mas.to
🌎 https://aleksandra.codes
Paris Fashion Week Feb '23
🥴
😭
Boilerplate
Lost typesafety
Repetitive error handling
🤔
Fullstack TypeScript app
TYPE-SAFE
TYPE-SAFE
// one-computer.js
function welcome(name) {
return `Hello, ${name}!`
}
const greeting = welcome("React Miami!")
// ^ "Hello, React Miami!"
// server.js
function welcome(name) {
return `Hello, ${name}!`
}
startImaginaryServer({ welcome })
// client.js
const greeting = await fetch(
`https://aleksandra.says/rpc/welcome`,
{ body: JSON.stringify("React Miami") }
)
😌
🥴
now we're looking for sth
NOT ÇORBA
AND NOT COBRA
module Finance {
typedef sequence<string> StringSeq;
struct AccountDetails {
string name;
StringSeq address;
long account_number;
double current_balance;
};
exception insufficientFunds { };
interface Account {
void deposit(in double amount);
void withdraw(in double amount)
raises(insufficientFunds);
readonly attribute AccountDetails details;
};
};
Developer trying to learn Corba
now we need
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2003/05/soap-envelope/"
soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<soap:Body>
<m:GetUserResponse>
<m:Username>Tony Stark</m:Username>
</m:GetUserResponse>
</soap:Body>
</soap:Envelope>
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header>
</soap:Header>
<soap:Body>
<m:GetUser>
<m:UserId>123</m:UserId>
</m:GetUser>
</soap:Body>
</soap:Envelope>
now we're looking for sth
When the web started to change
Can request and update resources
Exposes resources
Operation | RPC | REST |
---|---|---|
Login | POST /login | POST /sessions |
Logout | POST /logout | DELETE /sessions |
Get user by id | GET /getUser?id=123 | GET /users/123 |
Get user's todo items | GET /getTodos?userId=123 | GET /users/123/todos |
Add new todo item | POST /addTodo | POST users/123/todos |
Update todo item | POST /updateTodo | PUT /todos/1 |
Delete todo item | POST /deteteTodo | DELETE /todos/1 |
JSON-RPC
RESTful
"REST"
time for something
API
App
GET users/
GET tasks/
GET tags/
API
App
POST graphql/
Body:
{ "query": "query { users {...} }" }
User 1
Task 1
Task 2
Tag 1
Tag 2
query {
user(id: 1) {
name
tasks {
name
status
tags {
id
}
}
}
}
name
surname
age
status
name
priority
name
priority
status
description
id
Tag 3
id
description
id
description
id
at least until stuff like Max Stoiber's GraphQL CDN popped up
for free and out of the box
Revisiting the original promise of RPC
?
Source: https://www.vice.com/en/article/m7azaa/best-baggy-oversized-clothes
Fullstack TypeScript app
Fullstack TypeScript app
Fullstack TypeScript app
tRPC query & mutation procedures
Remix loader pattern
React Server Components
Qwik
Blitz RPC query & mutation resolvers
// src/queries/getProject.ts (runs on the server)
import db from "db"
import * as z from "zod"
const GetProject = z.object({
id: z.number(),
})
export default async function getProject(
input: z.infer<typeof GetProject>
) {
// Validate the input
const data = GetProject.parse(input)
const project = await db.project.findOne({ where: { id: data.id } })
// Can do any processing, fetching from other APIs, etc
return project
}
// src/pages/index.tsx (runs on the client & server)
import { useQuery } from "@blitzjs/rpc"
import getProject from "src/projects/queries/getProject"
function Index() {
const [project] = useQuery(getProject, { where: { id: 1 } })
return <span>{project.id}</span>
}
// src/pages/index.tsx (runs on the client & server)
import { useQuery } from "@blitzjs/rpc"
import getProject from "src/projects/queries/getProject"
function App() {
const [project] = useQuery(getProject, { where: { id: 1 } })
return <span>{project.id}</span>
}
// app/products/mutations/creaateProject.tsx
import db from "db"
import * as z from "zod"
const CreateProject = z
.object({
name: z.string(),
})
.nonstrict()
export default async function createProject(
input: z.infer<typeof CreateProject>
) {
const data = CreateProject.parse(input)
const project = await db.project.create({ data })
// Can do any processing, fetching from other APIs, etc
return project
}
import { useMutation } from '@blitzjs/rpc'
import updateProject from 'app/projects/mutations/updateProject'
function ProjectForm(props) {
const [updateProjectMutation] = useMutation(updateProject)
return (
<form
onSubmit={async values => {
try {
const project = await updateProjectMutation(values)
} catch (error) {
alert('Error saving project')
}
}}>
{/* ... */}
</form>
)
}
// src/server/appRouter.ts
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
import db from './db'
const t = initTRPC.create();
export const appRouter = t.router({
getProject: t.procedure
.input(
z.object({ id: z.number() }),
)
.query(({ input }) => {
const project = await db.project.findOne({ where: { id: input.id } })
return project
}),
});
export type AppRouter = typeof appRouter
// src/trpc-client.ts
import type { AppRouter } from '../server/appRouter'; // Magic happens here
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
};
},
ssr: true,
});
// src/pages/index.tsx
import { trpc } from '../trpc-client'
function Index() {
const { data } = trpc.getProject.useQuery({ id: 1 })
return <span>{data.id}</span>;
}
export const trpc = createTRPCNext<AppRouter>({ /* ... */});
// ---
function ProjectForm(props) {
const updateProjectMutation = trpc.createProject.useMutation()
return (
<form
onSubmit={values => {
try {
const project = mutation.mutate(values)
} catch (error) {
alert('Error saving project')
}
}}>
{/* ... */}
</form>
)
}
- @builder.io/qwik-city
- @tanstack/bling
import { useQuery } from '@tanstack/react-query';
import { server$ } from '@tanstack/bling';
export default function Index() {
const { isLoading, error, data } = useQuery({
queryKey: ['project'],
queryFn: server$(async () => { // 🤯
return await db.project.findOne({ where: { id: input.id } });
})
})
if (isLoading) return <span>Loading...</span>;
if (error) throw error;
return <span>{data.id}</span>;
}
// components/Counter.tsx
'use client'; // 👀
import { useState } from 'react';
export function Counter() {
const [state, setState] = useState(0)
return <button onClick={() => setState(s => s + 1)}>increment: {state}</button>
}
// app/page.tsx
import { db } from '../db';
import { Counter } from '../components/Counter.tsx';
export default function Page() {
const posts = await db.posts.select('name', 'slug');
return (
<div>
<Counter />
<ul>
{posts.map(post => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.name}</a>
</li>
))}
</ul>
</div>
)
}
Source: https://twitter.com/markdalgleish/status/1256800146118959109
Chloé, Spring 2023
2001
import { createClient, Mutable, OASOutput } from 'fets';
import type oas from './openapi';
const client = createClient<typeof oas>({
endpoint: 'http://localhost:3000',
});
type Project = OASOutput<typeof oas, '/project/{id}', 'get'>;
// openapi.ts
export default { openapi: '3.0.0' /* ... */ } as const
import { createClient, Mutable, OASOutput } from 'fets';
import type oas from './openapi';
const client = createClient<typeof oas>({
endpoint: 'http://localhost:3000',
});
type Project = OASOutput<typeof oas, '/project/{id}', 'get'>;
const newProjectRes = await client['/todo'].put({
json: {
content: { name: "New Project" }
},
});
const newProjectJson = await newProjectRes.json();
const getTodosRes = await client['/projects'].get();
// Deleting the project
const deleteProjectRes = await client['/project/{id}'].delete({
params: {
id: newProjectJson.id,
},
});
if (!deleteProjectRes.ok) {
console.error('Failed to delete project');
}
😌 Type-safety out of the box
🚀 No runtime overhead
💪 Supports Node.js, Deno, BUN,
Cloudflare Workers, AWS Lambda
🔎 IDE features
?
Source: http://www.ashleyedavidson.com/blog/the-history-of-fashion-diffusion-in-pictures
Know your options
Take what fits you
Tailor to your needs
@aleksandrasays
www.aleksandra.codes
@aleksandra@mas.to