Take a Rest
from REST
Aleksandra Sikora, @aleksandrasays
whoami
- open-source engineer @
- π§ββοΈ
- org of WrocΕaw
previously
- .js Maintainer
- cc
π¦ @aleksandrasays
π @beerose
π @aleksandra@mas.to
π https://aleksandra.codes






Software β Fashion
π€―
your work project VS your side project



Paris Fashion Week Feb '23



π π» π




πΊ
Why are we talking about APIs?
Server & Client
Server & Client
π₯΄
Server & Client
π
API Layer Problems
Boilerplate
Lost typesafety
Repetitive error handling
π€
API Layer Problems
Fullstack TypeScript app

TYPE-SAFE
TYPE-SAFE

RPC
1981

What is RPC?
// 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") } )
What is RPC?
calling remote procedures as if they were local
π
π₯΄
RPC -> non-agnostic
RPC -> non-agnostic
βββββ
now we're looking for sth
object oriented programming
CORBA
1991
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; }; };
IDL

Developer trying to learn Corba
CORBA -> complex
simple
- -
CORBA -> complex
now we need
SOAP
1998
<?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>
π βοΈ
SOAP -> heavy
light
-
SOAP -> heavy
now we're looking for sth
REST
2000
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 |
RPC vs. REST
JSON-RPC
RESTful
"REST"
REST -> inflexible
-
REST -> inflexible
time for something
GraphQL
2012
REST API
GraphQL API
API
App
GET users/
GET tasks/
GET tags/
API
App
POST graphql/
Body:
{ "query": "query { users {...} }" }
vs
Client controls the data it gets
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

GraphQL -> extra work & type-safety
-
GraphQL -> extra work & type-safety
-
-
for free and out of the box
Yves Saint Laurent
1968
2020


RPC
2020
Revisiting the original promise of RPC
1981
?


π¦
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
Blitz RPC
// 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 }
Blitz RPC
// 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> }
Blitz RPC
// 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> }
π€
tRPC
// 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
tRPC
// 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>; }
RPC in the same file as in:
- @builder.io/qwik-city
- @tanstack/bling
server$
server$
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>; }
React Server Components
// 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

again, non-agnostic
type-safety is a must



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'>;
FETS
// 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'>;
FETS
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'); }
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'); }
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'); }
FETS
π Type-safety out of the box
π No runtime overhead
πͺ Supports Node.js, Deno, BUN,
Cloudflare Workers, AWS Lambda
π IDE features
Summary

Know your options

Take what fits you



Thank you!
@aleksandrasays
www.aleksandra.codes
@aleksandra@mas.to

Miami: Take a Rest From REST
By Aleksandra Sikora
Miami: Take a Rest From REST
- 2,015