SOMOD supports creating the Web UI using NextJS pages.
NextJs is a complete framework in ReactJs with support for routing, static site generation, server-side rendering, image optimization, ...etc
SOMOD helps to create and reuse NextJS pages in modules.
In the example from the previous chapter, we have created REST APIs for user management. Now let us create the UI pages for these APIs by following the below steps.
Choose a Styling library
A styling library is an essential part of Web UI development. It defines the look and feel of the whole application. SOMOD works with any styling libraries used with NextJS and React.
For this example, let us install and configure Material UI
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
Create the following pages under the ui
directory
ui/pages/_document.tsx
// ui/pages/_document.tsx // This adds Roboto font across all pages import { Head, Html, Main, NextScript } from "next/document"; const Document = () => { return ( <Html> <Head> <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); }; export default Document;
ui/pages/_app.tsx
// ui/pages/_app.tsx import { CssBaseline } from "@mui/material"; import { NextComponentType } from "next"; import { AppProps } from "next/app"; import Head from "next/head"; import { FunctionComponent } from "react"; const App: FunctionComponent<AppProps & { Component: NextComponentType }> = ({ Component, pageProps }) => { return ( <> <Head> <meta name="viewport" content="initial-scale=1, width=device-width" /> </Head> <CssBaseline enableColorScheme /> <Component {...pageProps} /> </> ); }; export default App;
ui/pages/index.tsx
// ui/pages/index.tsx import { Box, Button, Link, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; const GettingStartedHome: NextComponentType = () => { return ( <Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" height="100vh" > <Typography variant="h1">User Management</Typography> <Typography variant="h4"> A Getting Started Project from{" "} <Link href="https://somod.dev" target="_blank"> SOMOD </Link> </Typography> <NextLink href="/user/list" passHref> <Button variant="contained" sx={{ m: 2 }}> Manage Users </Button> </NextLink> </Box> ); }; export default GettingStartedHome;
/ui/pages/user/list.tsx
// /ui/pages/user/list.tsx import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; import EditIcon from "@mui/icons-material/Edit"; import { Box, Button, Container, IconButton, Link, List, ListItem, ListItemText, Paper, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useEffect, useState } from "react"; import { UserWithId } from "../../../lib/types"; const deleteUser = async (userId: string) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId, { method: "DELETE" } ); await response.text(); }; const fetchUsers = async () => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/list" ); const users = await response.json(); return users as UserWithId[]; }; const Users: NextComponentType = () => { const [users, setUsers] = useState<UserWithId[]>([]); useEffect(() => { fetchUsers().then(result => { setUsers(result); }); }, []); const onDeleteUser = (userId: string) => { deleteUser(userId).then(() => { const newUsers = users.filter(u => u.userId != userId); setUsers(newUsers); }); }; return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">Users</Typography> <NextLink href="/user/create" passHref> <Button variant="text" sx={{ float: "right" }}> Create New User </Button> </NextLink> </Box> <Paper> <List> {users.map(user => ( <ListItem key={user.userId} secondaryAction={ <> <NextLink href={"/user/" + user.userId + "/edit"} passHref > <IconButton> <EditIcon /> </IconButton> </NextLink> <IconButton onClick={() => { onDeleteUser(user.userId); }} > <DeleteForeverIcon /> </IconButton> </> } > <ListItemText primary={ <NextLink href={"/user/" + user.userId} passHref> <Link>{user.name}</Link> </NextLink> } secondary={user.email} /> </ListItem> ))} </List> </Paper> </Container> ); }; export default Users;
/ui/pages/user/create.tsx
// /ui/pages/user/create.tsx import { Box, Button, Container, Paper, Stack, Switch, TextField, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useRouter } from "next/router"; import { useState } from "react"; import { CreateUserInput, UserWithId } from "../../../lib/types"; const createUser = async (user: CreateUserInput) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user", { method: "POST", body: JSON.stringify(user), headers: { "Content-Type": "application/json" } } ); const createdUser = await response.json(); return createdUser as UserWithId; }; const UserCreate: NextComponentType = () => { const [user, setUser] = useState<CreateUserInput>({ name: "", email: "", active: true, dob: "" }); const router = useRouter(); const updateName = (name: string) => { setUser({ ...user, name }); }; const updateEmail = (email: string) => { setUser({ ...user, email }); }; const updateDob = (dob: string) => { setUser({ ...user, dob }); }; const updateActive = (active: boolean) => { setUser({ ...user, active }); }; const submit = () => { createUser({ name: user.name, email: user.email, dob: user.dob, active: user.active }).then(result => { router.push("/user/" + result.userId); }); }; return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">Create New User</Typography> <Paper sx={{ p: 2 }}> <Stack spacing={2}> <Box> <TextField value={user.name} label="Name" fullWidth required onChange={event => { updateName(event.target.value); }} /> </Box> <Box> <TextField value={user.email} label="Email" fullWidth required onChange={event => { updateEmail(event.target.value); }} /> </Box> <Box> <TextField value={user.dob} label="Date of Birth" fullWidth placeholder="dd/mm/yyyy" InputLabelProps={{ shrink: true }} onChange={event => { updateDob(event.target.value); }} /> </Box> <Box> <Typography variant="subtitle2">Active</Typography> <Switch checked={user.active} size="small" onChange={event => { updateActive(event.target.checked); }} /> </Box> <Button variant="contained" onClick={submit}> Create </Button> <NextLink href={"/user/list"} passHref> <Button variant="text" color="info"> Back to User List </Button> </NextLink> </Stack> </Paper> </Box> </Container> ); }; export default UserCreate;
/ui/pages/user/[userId].tsx
// /ui/pages/user/[userId].tsx import { Box, Button, Container, Paper, Skeleton, Stack, Switch, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { UserWithId } from "../../../lib/types"; const fetchUser = async (userId: string) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId ); const users = await response.json(); return users as UserWithId; }; const UserView: NextComponentType = () => { const [user, setUser] = useState<UserWithId>(); const router = useRouter(); const { userId } = router.query; useEffect(() => { if (userId) { fetchUser(userId as string).then(result => { setUser(result); }); } }, [userId]); return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">User</Typography> <Paper sx={{ p: 2 }}> <Stack spacing={2}> {user ? ( <> <Box> <Typography variant="caption">{user.userId}</Typography> </Box> <Box> <Typography variant="subtitle2">Name</Typography> <Typography variant="body2">{user.name}</Typography> </Box> <Box> <Typography variant="subtitle2">Email</Typography> <Typography variant="body2">{user.email}</Typography> </Box> <Box> <Typography variant="subtitle2"> Date of Birth </Typography> <Typography variant="body2">{user.dob}</Typography> </Box> <Box> <Typography variant="subtitle2">Active</Typography> <Switch readOnly checked={user.active} size="small" /> </Box> <Box> <Typography variant="subtitle2"> Last Updated At </Typography> <Typography variant="body2"> {new Date(user.lastUpdatedAt).toDateString()} </Typography> </Box> <Box> <Typography variant="subtitle2">Created At</Typography> <Typography variant="body2"> {new Date(user.createdAt).toDateString()} </Typography> </Box> <NextLink href={"/user/" + user.userId + "/edit"} passHref> <Button variant="outlined">Edit</Button> </NextLink> <NextLink href={"/user/list"} passHref> <Button variant="text" color="info"> Back to User List </Button> </NextLink> </> ) : ( <> <Skeleton variant="rounded" height={20} /> <Skeleton variant="rounded" height={60} /> <Skeleton variant="rounded" height={60} /> </> )} </Stack> </Paper> </Box> </Container> ); }; export default UserView;
/ui/pages/user/[userId]/edit.tsx
// /ui/pages/user/[userId]/edit.tsx import { Box, Button, Container, Paper, Skeleton, Stack, Switch, TextField, Typography } from "@mui/material"; import { NextComponentType } from "next"; import NextLink from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { UpdateUserInput, UserWithId } from "../../../../lib/types"; const fetchUser = async (userId: string) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId ); const user = await response.json(); return user as UserWithId; }; const updateUser = async (userId: string, user: UpdateUserInput) => { const response = await fetch( process.env.NEXT_PUBLIC_USER_API_URL + "/user/" + userId, { method: "PUT", body: JSON.stringify(user), headers: { "Content-Type": "application/json" } } ); const updatedUser = await response.json(); return updatedUser as UserWithId; }; const UserEdit: NextComponentType = () => { const [user, setUser] = useState<UserWithId>(); const router = useRouter(); const { userId } = router.query; useEffect(() => { if (userId) { fetchUser(userId as string).then(result => { setUser(result); }); } }, [userId]); const updateName = (name: string) => { setUser({ ...user, name }); }; const updateEmail = (email: string) => { setUser({ ...user, email }); }; const updateDob = (dob: string) => { setUser({ ...user, dob }); }; const updateActive = (active: boolean) => { setUser({ ...user, active }); }; const submit = () => { updateUser(user.userId, { name: user.name, email: user.email, dob: user.dob, active: user.active }).then(() => { router.push("/user/" + user.userId); }); }; return ( <Container maxWidth="sm"> <Box my={3} p={1}> <Typography variant="h4">Edit User</Typography> <Paper sx={{ p: 2 }}> <Stack spacing={2}> {user ? ( <> <Box> <Typography variant="caption">{user.userId}</Typography> </Box> <Box> <TextField value={user.name} label="Name" fullWidth required onChange={event => { updateName(event.target.value); }} /> </Box> <Box> <TextField value={user.email} label="Email" fullWidth required onChange={event => { updateEmail(event.target.value); }} /> </Box> <Box> <TextField value={user.dob} label="Date of Birth" fullWidth placeholder="dd/mm/yyyy" InputLabelProps={{ shrink: true }} onChange={event => { updateDob(event.target.value); }} /> </Box> <Box> <Typography variant="subtitle2">Active</Typography> <Switch checked={user.active} size="small" onChange={event => { updateActive(event.target.checked); }} /> </Box> <Button variant="contained" onClick={submit}> Submit </Button> <NextLink href={"/user/" + user.userId} passHref> <Button variant="outlined" color="secondary"> Cancel </Button> </NextLink> </> ) : ( <> <Skeleton variant="rounded" height={20} /> <Skeleton variant="rounded" height={60} /> <Skeleton variant="rounded" height={60} /> </> )} </Stack> </Paper> </Box> </Container> ); }; export default UserEdit;
/ui/config.yaml
Let us use the UI Configuration to bind the API Endpoint URL to the FrontEnd
# yaml-language-server: $schema=../node_modules/somod-schema/schemas/ui-config/index.json env: NEXT_PUBLIC_USER_API_URL: # adds NEXT_PUBLIC_USER_API_URL environmental variable to nextjs project SOMOD::Parameter: apigateway.http.endpoint # The value of the environmental variable is read from the SOMOD Parameter apigateway.http.endpoint in parameters.json
Start a dev server for the UI
npx somod start --dev -v
Open the URL http://localhost:3000
in the browser to see the User Management App
Now the Module is ready with Backend and Frontend Code working together. In the Next Chapter, let us understand how to build and ship the SOMOD module.
Write an email to opensource@sodaru.com