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

👗 💻 📈

🗺

What is an API?

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

😌

🥴

  • Client & server tightly coupled
  • Having to use the same language
  • Need to learn all the procedure names
  • Having to use multi-threaded servers
  • Parameters marshalling
  • Exception handling

Problems with RPC

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

  • Complexity
  • Steep learning curve
  • Mapping problems
  • Name confused with a poisonous snake

Problems with 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>

🔐 ✈️

  • Heavy, requires more bandwidth
  • POST = no cache on HTTP layer
  • Tightly coupled with server
  • Inflexible

Problems with SOAP

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"

  • Over fetching
  • Big payloads
  • n+1 problem
  • Limiting constraints
  • No end-to-end typesafety

Problems with 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

  • Same POST-caching problem as in SOAP
  • You have to generate types
  • If you use tools like Hasura,
    you push a lot of domain logic to frontend
  • Otherwise — boilerplate!

Problems with GraphQL

at least until stuff like Max Stoiber's GraphQL CDN popped up

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>
}

🤔

Blitz RPC

// 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
}

Blitz RPC

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>
  )
}

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>;
}

tRPC

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>
  )
}

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

// 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>
}

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

once again, we'd like something 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');
}

FETS

😌 Type-safety out of the box

🚀 No runtime overhead

💪 Supports Node.js, Deno, BUN,

     Cloudflare Workers, AWS Lambda

🔎 IDE features

Summary

?

Source: http://www.ashleyedavidson.com/blog/the-history-of-fashion-diffusion-in-pictures

Know your options

Take what fits you

Tailor to your needs

Thank you!

@aleksandrasays

www.aleksandra.codes

@aleksandra@mas.to

Miami: Take a Rest From REST

By Aleksandra Sikora

Miami: Take a Rest From REST

  • 1,609