Como hospedar um aplicativo Remix?
Neste artigo, você aprenderá tudo o que precisa saber para começar a usar o Remix. Veremos seus prós e contras, demonstraremos como criar um aplicativo Remix e, por fim, como implantar o Remix.
Contents
O que é Remix?
O Remix é uma estrutura moderna da Web de pilha completa para a criação de aplicativos rápidos, elegantes e resilientes. Ele aproveita o poder da renderização no lado do servidor (SSR), permitindo tempos de carregamento mais rápidos e SEO aprimorado para aplicativos dinâmicos da Web.
Com o Remix, os desenvolvedores podem integrar perfeitamente a lógica do lado do cliente e do lado do servidor em uma base de código unificada. Em comparação com outros frameworks React populares, o Remix faz muitas coisas de forma diferente. Isso inclui:
- Roteamento baseado em arquivo (um arquivo no diretório de rotas representa uma rota específica)
- Tratamento tradicional de formulários (usando HTML e solicitações HTTP, semelhante ao PHP)
- Gerenciamento de estado (o estado é armazenado apenas no servidor)
- Carregamento de dados (desacoplado dos componentes)
O Remix foi criado em 2020 e inicialmente estava disponível sob uma taxa de licença anual. Posteriormente, em outubro de 2021, a equipe do Remix decidiu abrir o código-fonte do projeto. Agora ele está disponível sob a licença MIT.
No final de 2022, a Remix foi adquirida pela Shopify por 2,1 bilhões de dólares.
Benefícios do Remix
Vejamos os principais benefícios de usar a estrutura Remix.
O Remix foi desenvolvido com base no React Router, uma poderosa solução de roteamento do lado do cliente. Na verdade, o Remix foi criado pela mesma equipe de desenvolvedores que criou o React Router.
A estrutura utiliza um sistema de navegação baseado em arquivos, o que simplifica a organização do código. Ele permite que os desenvolvedores associem rotas, componentes e recursos a arquivos ou diretórios específicos.
Veja um exemplo:
app/
└── routes/
├── $noteId.tsx // matches: /<noteId>/
├── $noteId_.destroy.tsx // matches: /<noteId>/destroy
├── $noteId_.edit.tsx // matches: /<noteId>/edit
├── _index.tsx // matches: /
└── create.tsx // matches: /create
O melhor de tudo é que ele suporta roteamento aninhado via . Isso resulta em tempos de carregamento mais curtos, melhor tratamento de erros e muito mais!
Renderização no lado do servidor (SSR)
O Remix aproveita o poder da renderização no lado do servidor.
Em aplicativos React básicos, os dados geralmente são obtidos no lado do cliente e, em seguida, injetados no DOM. Isso é conhecido como renderização no lado do cliente (CSR).
O Remix, por outro lado, adota uma abordagem diferente. Primeiro, ele busca os dados no back-end, renderiza o HTML com os dados buscados e, em seguida, fornece-os ao cliente.
A renderização no lado do servidor tende a resultar em melhor desempenho e em aplicativos mais amigáveis para SEO.
Manuseio de formulários
O Remix leva o manuseio da forma de volta ao básico.
Em vez de usar vários componentes controlados e JavaScript, ele usa formulários HTML tradicionais e solicitações HTTP.
Quando um formulário é enviado, ele envia uma solicitação HTTP para uma rota específica, que é processada no lado do servidor usando a função action()
. Ele funciona de forma semelhante ao bom e velho PHP.
Isso significa que o Remix não requer absolutamente nenhum JavaScript para processar formulários. Embora isso seja ótimo, pode tornar a validação de formulários e a exibição de erros um pouco mais complicadas.
Gerenciamento do Estado
Nesse contexto, o estado refere-se à sincronização dos dados do servidor e do cliente.
O Remix facilita muito o gerenciamento de estado. Ele elimina a necessidade de Redux, Zustand, React Query, Apollo ou qualquer outra biblioteca de gerenciamento de estado do lado do cliente.
Ao usar o Remix, todo o estado é tratado pelo servidor. O cliente não mantém praticamente nenhum estado; portanto, o processo de sincronização não é necessário.
Os dados do servidor para o cliente são transmitidos por meio de várias funções, como loader()
e action()
.
Transições e UI otimista
As transições Remix tornam a movimentação entre as páginas suave e rápida, carregando dados antecipadamente e usando animações.
A UI otimista reflete instantaneamente as ações do usuário, tornando os sites mais responsivos ao mostrar as alterações antes que elas sejam confirmadas.
As transições e a IU otimista melhoram muito a experiência do usuário, reduzindo a latência percebida e fornecendo feedback imediato.
Limitações do Remix
Embora o Remix seja excelente, ele tem algumas limitações.
Complexo
O Remix não é a estrutura mais fácil de usar, e sua documentação (no momento em que este artigo foi escrito) não é das melhores.
A estrutura também faz muitas coisas de forma diferente do React original. Se você for um desenvolvedor React, talvez leve algum tempo para entender todos os diferentes conceitos do Remix.
Menos popular
O Remix ainda é uma estrutura relativamente nova. Seu lançamento público data apenas de outubro de 2021. Ele é menos maduro e testado em batalha em comparação com alguns concorrentes, como o Next.js.
Observando as estrelas do GitHub, podemos ver que o Remix (27 mil estrelas) é muito menos popular que o Next.js (120 mil estrelas). No momento em que este artigo foi escrito, o Remix não foi adotado por nenhum gigante da tecnologia, exceto pelo Shopify.
Acoplado firmemente
Os aplicativos Remix têm um frontend e um backend fortemente acoplados. Embora essa abordagem funcione para projetos menores, alguns desenvolvedores preferem a flexibilidade de um frontend e backend separados.
Além disso, separar o frontend do backend pode tornar seu código mais fácil de manter e de testar.
Sem SSG ou ISR
O Remix não oferece suporte à geração de sites estáticos (SSG) nem à regeneração estática incremental (ISR).
Como implantar um aplicativo Remix?
Nesta seção, criaremos e implantaremos um aplicativo Remix.
Pré-requisitos
Para acompanhar o processo, você precisará de:
- Conhecimento básico de TypeScript
- Experiência com o Docker (e tecnologia de contêineres)
- Node.js e um IDE JavaScript instalado em seu computador
- Docker Desktop instalado em seu computador
Visão geral do projeto
Ao longo deste artigo, trabalharemos em um aplicativo Web de anotações. O aplicativo Web permitirá que os usuários criem, recuperem, editem e excluam notas.
Para o backend, utilizaremos o Back4app BaaS, e para o frontend, usaremos a estrutura Remix. Depois que o frontend estiver codificado, nós o implantaremos nos contêineres do Back4app.
O produto final terá a seguinte aparência:
Sugiro que você acompanhe primeiro o aplicativo Web Notes. Após o artigo, você deverá ser capaz de implementar seus próprios aplicativos Remix.
Backend
Nesta seção do artigo, usaremos o Back4app para criar o backend do nosso aplicativo.
Criar aplicativo
Comece fazendo login na sua conta do Back4app (ou criando uma, se ainda precisar obtê-la).
Ao fazer login, você será redirecionado para o portal “My Apps”. Para criar um backend, primeiro você precisa criar um aplicativo Back4app. Para fazer isso, clique em “Build new app” (Criar novo aplicativo).
O Back4app oferece duas soluções:
- Backend as a Service (BaaS): uma solução de backend totalmente desenvolvida
- Contêineres como serviço (CaaS) — plataforma de orquestração de contêineres baseada em Docker
Como estamos trabalhando em um backend, selecione “Backend as a Service”.
Dê um nome ao seu aplicativo, deixe todo o resto como padrão e clique em “Create” (Criar).
A plataforma levará algum tempo para configurar tudo o que é necessário para seu backend. Isso inclui o banco de dados, a interface do aplicativo, o dimensionamento, a segurança etc.
Quando seu aplicativo estiver pronto, você será redirecionado para a visualização do banco de dados em tempo real do aplicativo.
Banco de dados
Continuando, vamos cuidar do banco de dados.
Como estamos criando um aplicativo relativamente simples, precisaremos apenas de um modelo – vamos chamá-lo de Note
. Para criá-lo, clique no botão “Create a class” (Criar uma classe) no canto superior esquerdo da tela.
Dê a ela o nome de Note
, ative “Public Read and Write Enabled” e clique em “Create class & add columns”.
Em seguida, adicione as seguintes colunas:
+-----------+--------------+----------------+----------+
| Data type | Name | Default value | Required |
+-----------+--------------+----------------+----------+
| String | emoji | <leave blank> | yes |
+-----------+--------------+----------------+----------+
| String | title | <leave blank> | yes |
+-----------+--------------+----------------+----------+
| File | content | <leave blank> | no |
+-----------+--------------+----------------+----------+
Depois de criar a classe, preencha o banco de dados com dados de amostra. Crie algumas notas fornecendo os emojis, os títulos e o conteúdo. Como alternativa, importe essa exportação do banco de dados.
Ótimo, é isso!
Criamos com sucesso um backend sem escrever nenhum código.
Para saber mais sobre o Backend as a Service, confira o artigo O que é Backend as a Service?
Front-end
Nesta seção do artigo, criaremos o frontend do nosso aplicativo usando a estrutura Remix.
criar-remixar
A maneira mais fácil de inicializar um projeto Remix é por meio do utilitário create-remix
. Essa ferramenta cria um projeto Remix pronto para produção.
Ele cuida da estrutura de diretórios e dependências, configura o empacotador, etc.
Crie um novo projeto Remix executando o seguinte comando:
$ npx create-remix@latest remix-notes
Initialize a new git repository? Yes
Install dependencies with npm? Yes
Se você nunca usou
o create-remix
, ele será instalado automaticamente.
Depois que o projeto tiver sido criado, altere seu diretório ativo para ele:
$ cd remix-notes
Por fim, inicie o servidor de desenvolvimento:
$ npm run dev
Abra seu navegador da Web favorito e navegue até http://localhost:5173. A página inicial padrão do Remix deve aparecer na sua tela.
TailwindCSS
Para facilitar um pouco nossa vida, usaremos o TailwindCSS. O TailwindCSS é uma estrutura que prioriza a utilidade e permite que você crie rapidamente front-ends sem escrever CSS simples.
Primeiro, instale-o usando o npm:
$ npm install -D tailwindcss postcss autoprefixer
Em seguida, execute tailwindcss init
:
$ npx tailwindcss init --ts -p
Isso configurará seu projeto e criará um arquivo tailwind.config.ts na raiz do projeto.
Modifique sua propriedade content
da seguinte forma para permitir que o Tailwind saiba em quais arquivos as classes de utilitários serão usadas:
// tailwind.config.ts
import type {Config} from "tailwindcss";
export default {
content: ["./app/**/*.{js,jsx,ts,tsx}"], // new
theme: {
extend: {},
},
plugins: [],
} satisfies Config;
Crie um novo arquivo chamado tailwind.css no diretório do aplicativo com o seguinte conteúdo:
/* app/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Por fim, importe-o no root.tsx por meio de links
:
// app/root.tsx
// other imports
import {LinksFunction} from "@remix-run/node";
import stylesheet from "~/tailwind.css?url";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
];
// ...
É isso aí!
Instalamos o TailwindCSS com sucesso.
Rotas
Nosso aplicativo Web terá os seguintes pontos de extremidade:
/
exibe todas as notas/create
permite que os usuários criem notas/
exibe uma nota específica/
/delete permite que os usuários excluam uma nota específica
Para definir essas rotas, crie a seguinte estrutura de diretórios na pasta do aplicativo:
app/
└── routes/
├── $noteId.tsx
├── $noteId_.destroy.tsx
├── $noteId_.edit.tsx
├── _index.tsx
└── create.tsx
Como você deve ter adivinhado, o prefixo $
é usado para parâmetros dinâmicos e .
é usado em vez de /
.
Visualizações
Continuando, vamos implementar as exibições.
Para tornar nosso código mais seguro quanto ao tipo, criaremos uma interface chamada Note
que se assemelha à nossa classe de banco de dados Note
.
Crie uma pasta chamada store e, dentro dela, crie NoteModel.ts com o seguinte conteúdo:
// app/store/NoteModel.ts
export default interface NoteModel {
objectId: string;
emoji: string;
title: string;
content: string;
createdAt: Date;
updatedAt: Date;
}
Em seguida, cole o código de visualização de _index.tsx, $noteId.tsx e create.tsx:
// app/routes/_index.tsx
import {Link, NavLink} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";
const notes = [
{objectId: "1", emoji: "📝", title: "My First Note"},
{objectId: "2", emoji: "📓", title: "My Second Note"},
{objectId: "3", emoji: "📔", title: "My Third Note"},
] as NoteModel[];
export default function Index() {
return (
<>
<Link to={`/create`}>
<div className="bg-blue-500 hover:bg-blue-600 text-lg font-semibold text-white
px-4 py-3 mb-2 border-2 border-blue-600 rounded-md"
>
+ Create
</div>
</Link>
{notes.map(note => (
<NavLink key={note.objectId} to={`/${note.objectId}`}>
<div className="hover:bg-slate-200 text-lg font-semibold
px-4 py-3 mb-2 border-2 border-slate-300 rounded-md"
>
{note.emoji} {note.title}
</div>
</NavLink>
))}
</>
);
}
// app/routes/$noteId.tsx
import {Form} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";
const note = {
objectId: "1", emoji: "📝", title: "My First Note", content: "Content here.",
createdAt: new Date(), updatedAt: new Date(),
} as NoteModel;
export default function NoteDetails() {
return (
<>
<div className="mb-4">
<p className="text-6xl">{note.emoji}</p>
</div>
<div className="mb-4">
<h2 className="font-semibold text-2xl">{note.title}</h2>
<p>{note.content}</p>
</div>
<div className="space-x-2">
<Form
method="post" action="destroy"
onSubmit={(event) => event.preventDefault()}
className="inline-block"
>
<button
type="submit"
className="bg-red-500 hover:bg-red-600 font-semibold text-white
p-2 border-2 border-red-600 rounded-md"
>
Delete
</button>
</Form>
</div>
</>
);
}
// app/routes/create.tsx
import {Form} from "@remix-run/react";
export default function NoteCreate() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-2xl">Create Note</h2>
</div>
<Form method="post" className="space-y-4">
<div>
<label htmlFor="emoji" className="block">Emoji</label>
<input
type="text" id="emoji" name="emoji"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<label htmlFor="title" className="block">Title</label>
<input
type="text" id="title" name="title"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<label htmlFor="content" className="block">Content</label>
<textarea
id="content" name="content"
className="w-full border-2 border-slate-300 p-2 rounded"
/>
</div>
<div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 font-semibold
text-white p-2 border-2 border-blue-600 rounded-md"
>
Create
</button>
</div>
</Form>
</>
);
}
Não há nada de sofisticado nesse código. Tudo o que fizemos foi usar JSX em combinação com TailwindCSS para criar a interface do usuário.
Como você deve ter notado, todos os componentes não são controlados (não estamos usando useState()
). Além disso, estamos usando um formulário HTML real.
Isso ocorre porque a estrutura Remix lida com formulários semelhantes ao PHP usando solicitações HTTP, ao contrário do React.
Parse
Há várias maneiras de se conectar ao seu backend baseado no Back4app. Você pode usar:
- API RESTful
- API GraphQL
- SDK do Parse
A maneira mais fácil e mais robusta é certamente o Parse SDK. O Parse SDK é um kit de desenvolvimento de software que fornece várias classes de utilitários e métodos para consultar e manipular facilmente seus dados.
Comece instalando o Parse via npm:
$ npm install -i parse @types/parse
Crie um arquivo .env na raiz do projeto da seguinte forma:
# .env
PARSE_APPLICATION_ID=<your_parse_app_id>
PARSE_JAVASCRIPT_KEY=<your_parse_js_key>
Certifique-se de substituir
<your_parse_app_id>
e<your_parse_js_key>
pelas credenciais reais. Para obter as credenciais, navegue até seu aplicativo e selecione “App Settings > Server & Security” na barra lateral.
Em seguida, inicialize o Parse na parte superior do arquivo root.tsx da seguinte forma:
// app/root.tsx
// other imports
import Parse from "parse/node";
const PARSE_APPLICATION_ID = process.env.PARSE_APPLICATION_ID || "";
const PARSE_HOST_URL = "https://parseapi.back4app.com/";
const PARSE_JAVASCRIPT_KEY = process.env.PARSE_JAVASCRIPT_KEY || "";
Parse.initialize(PARSE_APPLICATION_ID, PARSE_JAVASCRIPT_KEY);
Parse.serverURL = PARSE_HOST_URL;
Criaremos um arquivo separado chamado api/backend.ts para manter nossas exibições livres da lógica de comunicação do backend.
Crie api/backend.ts com o seguinte conteúdo:
// app/api/backend.ts
import Parse from "parse/node";
import NoteModel from "~/store/NoteModel";
export const serializeNote = (note: Parse.Object<Parse.Attributes>): NoteModel => {
return {
objectId: note.id,
emoji: note.get("emoji"),
title: note.get("title"),
content: note.get("content"),
createdAt: new Date(note.get("createdAt")),
updatedAt: new Date(note.get("updatedAt")),
};
}
export const getNotes = async (): Promise<NoteModel[]> => {
// ...
}
export const getNote = async (objectId: string): Promise<NoteModel | null> => {
// ...
}
// Grab the entire file from:
// https://github.com/duplxey/back4app-containers-remix/blob/master/app/api/backend.ts
Por fim, modifique as exibições para buscar e manipular os dados do backend:
// app/routes/index.tsx
import {json} from "@remix-run/node";
import {Link, NavLink, useLoaderData} from "@remix-run/react";
import {getNotes} from "~/api/backend";
export const loader = async () => {
const notes = await getNotes();
return json({notes});
};
export default function Index() {
const {notes} = useLoaderData<typeof loader>();
return (
// ...
);
}
// app/routes/$noteId.tsx
import {getNote} from "~/api/backend";
import {json, LoaderFunctionArgs} from "@remix-run/node";
import {Form, useLoaderData} from "@remix-run/react";
import {invariant} from "@remix-run/router/history";
export const loader = async ({params}: LoaderFunctionArgs) => {
invariant(params.noteId, "Missing noteId param");
const note = await getNote(params.noteId);
if (note == null) throw new Response("Not Found", {status: 404});
return json({note});
};
export default function NoteDetails() {
const {note} = useLoaderData<typeof loader>();
return (
// ...
);
}
// app/routes/create.tsx
import {ActionFunctionArgs, redirect} from "@remix-run/node";
import {Form} from "@remix-run/react";
import {createNote} from "~/api/backend";
export const action = async ({request}: ActionFunctionArgs) => {
const formData = await request.formData();
const {emoji, title, content} = Object.fromEntries(formData)
as Record<string, string>;
const note = await createNote(emoji, title, content);
return redirect(`/${note?.objectId}`);
};
export default function NoteCreate() {
return (
// ...
);
}
// app/routes/$noteId_.destroy.tsx
import type {ActionFunctionArgs} from "@remix-run/node";
import {redirect} from "@remix-run/node";
import {invariant} from "@remix-run/router/history";
import {deleteNote} from "~/api/backend";
export const action = async ({params}: ActionFunctionArgs) => {
invariant(params.noteId, "Missing noteId param");
await deleteNote(params.noteId);
return redirect(`/`);
};
Resumo do código:
- Usamos a função Remix
loader()
para carregar os dados. Depois que os dados foram carregados, nós os passamos para a exibição como JSON por meio da funçãojson()
.
- Usamos a função
action()
do Remix para lidar com o envio do formulário (por exemplo,POST
).
- O
noteId
foi passado para as visualizações como um parâmetro.
Agora, o aplicativo deve estar totalmente funcional e sincronizado com o backend do Back4app. Para garantir que tudo esteja funcionando, crie algumas notas, edite-as e, em seguida, exclua-as.
Aplicativo Dockerize
Nesta seção, vamos dockerizar nosso frontend do Remix.
Dockerfile
Um Dockerfile é um arquivo de texto simples que descreve as etapas que o mecanismo do Docker deve executar para construir uma imagem.
Essas etapas abrangem a definição do diretório de trabalho, a especificação da imagem de base, a transferência de arquivos, a execução de comandos e muito mais.
Normalmente, as instruções são representadas em letras maiúsculas e imediatamente seguidas por seus respectivos argumentos.
Para saber mais sobre todas as instruções, consulte a referência do Dockerfile.
Crie um Dockerfile na raiz do projeto com o seguinte conteúdo:
# Dockerfile
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]
Esse Dockerfile é baseado na imagem node:20
. Ele primeiro define o diretório de trabalho, copia o package.json e instala as dependências.
Depois disso, ele cria o projeto, expõe a porta 3000
e serve o aplicativo.
.dockerignore
Ao trabalhar com o Docker, você geralmente se esforça para criar imagens que sejam as menores possíveis.
Como nosso projeto contém determinados arquivos que não precisam estar na imagem (por exemplo, .git,
compilação,
configurações do IDE), nós os excluiremos. Para fazer isso, criaremos um arquivo .dockerignore que funciona de forma semelhante a um arquivo .gitignore.
Crie um arquivo .dockerignore na raiz do projeto:
# .dockerignore
.idea/
.cache/
build/
node_modules/
Adapte o .dockerignore de acordo com as necessidades de seu projeto.
Criação e teste
Antes de enviar uma imagem do Docker para a nuvem, é uma boa ideia testá-la localmente.
Primeiro, crie a imagem:
$ docker build -t remix-notes:1.0 .
Em seguida, crie um contêiner usando a imagem recém-criada:
$ docker run -it -p 3000:3000
-e PARSE_APPLICATION_ID=<your_parse_app_id>
-e PARSE_JAVASCRIPT_KEY=<your_parse_javascript_key>
-d remix-notes:1.0
Certifique-se de substituir
<your_parse_app_id>
e<your_parse_javascript_key>
pelas credenciais reais.
Seu aplicativo agora deve estar acessível em http://localhost:3000. Ele deve se comportar da mesma forma que antes do processo de dockerização.
Enviar para o GitHub
Para implantar um aplicativo no Back4app Containers, você deve primeiro enviar seu código-fonte para o GitHub. Para fazer isso, você pode seguir estas etapas:
- Faça login na sua conta do GitHub (ou registre-se).
- Crie um novo repositório do GitHub.
- Navegue até seu projeto local e inicialize-o:
git init
- Adicione todo o código ao sistema de controle de versão:
git add .
- Adicione a origem remota via
git remote add origin
- Faça o commit de todo o código via
git commit -m "initial commit"
- Envie o código para o GitHub
git push origin master
Implantar aplicativo
Nesta última seção, implantaremos o frontend nos contêineres do Back4app.
Faça login na sua conta do Back4app e clique em “Build new app” (Criar novo aplicativo) para iniciar o processo de criação do aplicativo.
Como agora estamos implantando um aplicativo em contêiner, selecione “Containers as a Service” (Contêineres como serviço).
Em seguida, você deve vincular sua conta do GitHub à Back4app e importar o repositório que criou anteriormente. Depois de conectado, selecione o repositório.
O Back4app Containers permite configurações avançadas. No entanto, para nosso aplicativo simples, as seguintes configurações serão suficientes:
- Nome do aplicativo: remix-notes (ou escolha seu nome)
- Variáveis de ambiente:
PARSE_APPLICATION_ID
,PARSE_JAVASCRIPT_KEY
Use os valores que você usou no arquivo .env para as variáveis de ambiente.
Quando terminar de configurar a implementação, clique em “Deploy” (Implementar).
Aguarde alguns instantes para que a implantação seja concluída. Depois de implantado, clique no link verde no lado esquerdo da tela para abrir o aplicativo no navegador.
Para obter um tutorial detalhado, consulte a documentação do Container Remix do Back4app.
Pronto! Seu aplicativo agora está implantado com sucesso e pode ser acessado pelo link fornecido. Além disso, a Back4app emitiu um certificado SSL gratuito para seu aplicativo.
Conclusão
Apesar de ser uma estrutura relativamente nova, o Remix permite que os desenvolvedores criem aplicativos da Web avançados de pilha completa.
A estrutura aborda muitas complexidades dos aplicativos da Web, como manipulação de formulários, gerenciamento de estado e muito mais.
Neste artigo, você aprendeu a criar e implantar um aplicativo Remix. Agora você deve ser capaz de utilizar o Back4app para criar um backend simples e o Back4app Containers para implantar seus aplicativos em contêineres.
O código-fonte do projeto está disponível no repositório back4app-containers-remix.